Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,39 @@
<WarningsAsErrors>$(WarningsAsErrors);NU1901;NU1902;NU1903;NU1904</WarningsAsErrors>
</PropertyGroup>

<!-- Package metadata for everything that ships from src/. A consumer, or an agent, that has
only the installed package learns the API from what is inside it: the XML doc file beside
the assembly, the README, and the description. A project overrides any of these in its own
csproj. -->
<PropertyGroup Condition="$(MSBuildProjectDirectory.StartsWith('$(MSBuildThisFileDirectory)src'))">
<Authors>Theauxm,mark-keaton</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://traxsharp.net/docs</PackageProjectUrl>
<RepositoryUrl>https://github.com/TraxSharp/Trax.Core</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageTags>trax;dotnet;railway-oriented-programming;functional;pipeline;either</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- CS1591 (public member has no doc comment) is the one doc warning left off. The existing
summaries ship now; documenting the members that have none is a tracked burn-down, and
every other doc-comment warning (malformed XML, a bad cref, a param mismatch) still
fails the build. Trax.Core.Testing already documents every public member, so it keeps
CS1591 on. -->
<NoWarn Condition="'$(MSBuildProjectName)' != 'Trax.Core.Testing'">$(NoWarn);CS1591</NoWarn>
<!-- No .snupkg: DotNet.ReproducibleBuilds (below) sets DebugType=embedded, so the PDB, with its
SourceLink map to this repository's commit, is already inside the shipped DLL. A symbol
package would hold no PDB at all. -->
</PropertyGroup>

<ItemGroup Condition="$(MSBuildProjectDirectory.StartsWith('$(MSBuildThisFileDirectory)src'))">
<None
Include="$(MSBuildThisFileDirectory)README.md"
Pack="true"
PackagePath="\"
Visible="false"
/>
</ItemGroup>

<!-- Package validation: `dotnet pack` compares each published library with the last release on
nuget.org and fails on a binary break, such as narrowing a member a published downstream
package overrides. It runs on pack only, so the pull request workflow packs too. Bump the
Expand Down
55 changes: 43 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@
[![NuGet Version](https://img.shields.io/nuget/v/Trax.Core)](https://www.nuget.org/packages/Trax.Core/)
[![NuGet Downloads](https://img.shields.io/nuget/dt/Trax.Core)](https://www.nuget.org/packages/Trax.Core/)
[![.NET](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/TraxSharp/Trax.Core/blob/main/LICENSE)
[![Last Commit](https://img.shields.io/github/last-commit/TraxSharp/Trax.Core)](https://github.com/TraxSharp/Trax.Core/commits/main)
[![codecov](https://codecov.io/gh/TraxSharp/Trax.Core/branch/main/graph/badge.svg)](https://codecov.io/gh/TraxSharp/Trax.Core)
[![Docs](https://img.shields.io/badge/docs-traxsharp.net-blue)](https://traxsharp.net/docs)

Railway Oriented Programming for .NET. Build trains that carry data through a sequence of stops, with automatic derailment handling when something goes wrong.

A **train** (`Train<TIn, TOut>`) declares a chain of **junctions** (`Junction<TIn, TOut>`), small classes that each do one thing. The train keeps every value it has seen in a type-keyed memory, hands each junction the input and constructor arguments it asks for, and stops at the first junction that throws, returning `Either<Exception, TOut>` instead of throwing. Trax.Core is the in-process foundation of the Trax packages; the layers above it add dependency injection, execution logging, dispatch, scheduling and a dashboard.

```bash
dotnet add package Trax.Core
dotnet add package Trax.Core.Testing # optional: architecture-guard test fixtures
```

Documentation: [traxsharp.net/docs](https://traxsharp.net/docs).

## The Trax Stack

Trax is a layered framework split across several repos. You can stop at whatever layer solves your problem. **You are here: Trax.Core.**
Expand Down Expand Up @@ -84,49 +93,71 @@ Requires `net10.0`.
dotnet add package Trax.Core
```

`Trax.Core.Analyzers` is deprecated and reports nothing; do not install it.

## Quick Start

**1. Define a junction.** Each junction takes one type of cargo in and produces one type of cargo out:
**1. Define junctions.** Each junction takes one type of cargo in and produces one type of cargo out. Its constructor arguments are taken from the train's memory:

```csharp
using LanguageExt;
using Trax.Core.Junction;
using Trax.Core.Train;

public record CreateUserRequest(string Email);
public record User(Guid Id, string Email);

public interface IUserRepository
{
Task<User?> GetByEmailAsync(string email);
Task<User> AddAsync(string email);
}

public class ValidateEmailJunction(IUserRepository repo) : Junction<CreateUserRequest, Unit>
{
public override async Task<Unit> Run(CreateUserRequest input)
{
var existing = await repo.GetByEmailAsync(input.Email);
if (existing is not null)
throw new ValidationException($"Email {input.Email} is already taken");
if (await repo.GetByEmailAsync(input.Email) is not null)
throw new InvalidOperationException($"Email {input.Email} is already taken");

return Unit.Default;
}
}

public class CreateUserInDatabaseJunction(IUserRepository repo) : Junction<CreateUserRequest, User>
{
public override Task<User> Run(CreateUserRequest input) => repo.AddAsync(input.Email);
}
```

**2. Build a route by chaining junctions into a train:**

```csharp
public class CreateUserTrain : Train<CreateUserRequest, User>
public class CreateUserTrain(IUserRepository repo) : Train<CreateUserRequest, User>
{
protected override Task<Either<Exception, User>> Junctions() =>
Chain<ValidateEmailJunction>()
AddServices(repo)
.Chain<ValidateEmailJunction>()
.Chain<CreateUserInDatabaseJunction>()
.Chain<SendWelcomeEmailJunction>()
.Resolve();
}
```

When the train is run with an input, the cargo is loaded automatically. At each stop, `.Chain<T>` picks up the cargo `T` needs from what the train is carrying, runs the junction, and loads the output back on. `Resolve` unloads the final delivery at the destination.
When the train is run with an input, the cargo is loaded automatically. `AddServices` puts the repository on board, so each junction's constructor can take it. At each stop, `.Chain<T>` picks up the cargo `T` needs from what the train is carrying, runs the junction, and loads the output back on. `Resolve` unloads the final delivery at the destination.

The train carries all of this in **Memory**, a type-keyed store that accumulates as the train moves through its route. Each stop can use anything a previous stop produced.

With Trax.Effect, a `ServiceTrain` resolves junction dependencies from the DI container instead, so `AddServices` is not needed.

**3. Run it:**

```csharp
var train = new CreateUserTrain();
Either<Exception, User> result = await train.RunEither(request);
// repo is any IUserRepository implementation
var train = new CreateUserTrain(repo);
Either<Exception, User> result = await train.RunEither(new CreateUserRequest("ada@example.com"));

// Or throw on failure:
User user = await train.Run(request);
User user = await new CreateUserTrain(repo).Run(new CreateUserRequest("grace@example.com"));
```

## Startup Chain Verification
Expand Down
5 changes: 4 additions & 1 deletion src/Trax.Core.Analyzers/Trax.Core.Analyzers.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
The Pack path in analyzers/dotnet/cs handles NuGet packaging separately. -->
<NoWarn>RS2008</NoWarn>
<!-- Shown on nuget.org and in the package manager, where a consumer decides to install it. -->
<Description>Deprecated. Checked chains rooted at Activate(), which can no longer be written, so it no longer checks anything. Trax verifies every registered train's chain at host startup instead.</Description>
<Description>Deprecated Roslyn analyzer for Trax.Core (diagnostics CHAIN001 and CHAIN002). It checked chains rooted at Activate(), which can no longer be written, so it no longer reports anything and there is no reason to install it. Trax verifies every registered train's chain at host startup instead.</Description>
<PackageTags>trax;dotnet;analyzer;roslyn;deprecated</PackageTags>
<!-- An analyzer, not a library a consumer calls: there is no API to document. -->
<GenerateDocumentationFile>false</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
Expand Down
5 changes: 1 addition & 4 deletions src/Trax.Core.Testing/Trax.Core.Testing.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Trax.Core.Testing</AssemblyName>
<Authors>Theauxm,mark-keaton</Authors>
<PackageDescription>Reusable architecture-guard infrastructure and hygiene checkers for Trax codebases, with NUnit base fixtures: subclass a fixture, supply options, and run dotnet test (no test bodies to write). The underlying checkers return offender lists if you prefer your own framework.</PackageDescription>
<PackageTags>trax;testing;architecture;guards;conventions;nunit</PackageTags>
<RepositoryUrl>https://github.com/TraxSharp/Trax.Core</RepositoryUrl>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>trax;dotnet;testing;architecture;guards;conventions;nunit</PackageTags>
</PropertyGroup>

<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions src/Trax.Core/Extensions/FunctionalExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static class FunctionalExtensions
/// <typeparam name="R">The Right type (the result type)</typeparam>
/// <param name="option">The Task of Either to unwrap</param>
/// <returns>The Right value if present</returns>
/// <exception cref="L">Thrown if the Either contains a Left value</exception>
/// <exception cref="Exception">The Left value, of type <typeparamref name="L"/>, rethrown if the Either holds one</exception>
/// <remarks>
/// This method is typically used at the boundary of a Railway-oriented system,
/// where you need to convert back to traditional exception handling.
Expand All @@ -44,7 +44,7 @@ internal static async Task<R> Unwrap<L, R>(this Task<Either<L, R>> option)
/// <typeparam name="R">The Right type (the result type)</typeparam>
/// <param name="option">The Either to unwrap</param>
/// <returns>The Right value if present</returns>
/// <exception cref="L">Thrown if the Either contains a Left value</exception>
/// <exception cref="Exception">The Left value, of type <typeparamref name="L"/>, rethrown if the Either holds one</exception>
/// <remarks>
/// This method is typically used at the boundary of a Railway-oriented system,
/// where you need to convert back to traditional exception handling.
Expand Down
4 changes: 2 additions & 2 deletions src/Trax.Core/Monad/Monad.ShortCircuit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Either<Exception, TOut> Result

/// <summary>
/// Executes a junction with short-circuit behavior. If the junction returns Right, its
/// TReturn value becomes what <see cref="Resolve"/> returns; if it returns Left, the failure
/// TReturn value becomes what <see cref="Resolve()"/> returns; if it returns Left, the failure
/// is ignored. The chain does not end here: later junctions still run, and a failure in one
/// of them still fails the chain.
/// </summary>
Expand All @@ -75,7 +75,7 @@ private Task<Monad<TInput, TReturn>> ShortCircuitAsync<TJunction>()

/// <summary>
/// Executes a junction with short-circuit behavior. If the junction returns Right, its
/// TReturn value becomes what <see cref="Resolve"/> returns; if it returns Left, the failure
/// TReturn value becomes what <see cref="Resolve()"/> returns; if it returns Left, the failure
/// is ignored. The chain does not end here: later junctions still run, and a failure in one
/// of them still fails the chain.
/// </summary>
Expand Down
8 changes: 1 addition & 7 deletions src/Trax.Core/Trax.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,10 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Authors>Theauxm,mark-keaton</Authors>
<PackageDescription>Railway Oriented .NET Programming</PackageDescription>
<RepositoryUrl>https://github.com/Theauxm/Trax.Core</RepositoryUrl>
<PackageDescription>Railway-oriented pipelines for .NET: a train chains junctions (small single-purpose classes), passes each one the cargo it needs from the train's type-keyed memory, and stops at the first failure, returning Either&lt;Exception, T&gt;. Install it on its own for in-process pipelines; add Trax.Effect for dependency injection, execution logging and persistence.</PackageDescription>
<AssemblyName>Trax.Core</AssemblyName>
</PropertyGroup>

<PropertyGroup>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="LanguageExt.Core" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
Expand Down
Loading