This repository is a modernized fork of ErrorOr by Amichai Mantinband.
The original project is licensed under the MIT License. This fork extends the library with additional features (.NET 10 support, new error types, JSON serialization, LINQ-style methods) while maintaining full API compatibility.
dotnet add package VsaResults
Loving it? Show your support by giving this project a star!
Install the package:
dotnet add package VsaResults
Create and consume an ErrorOr<T> in a few lines:
public static ErrorOr<int> Parse(string input)
=> int.TryParse(input, out var value)
? value
: Error.Validation("Parse.Invalid", "Input must be a number");
var message = Parse("42")
.Then(value => value * 2)
.Match(
value => $"Value: {value}",
errors => errors[0].Description);
Console.WriteLine(message);This π
public float Divide(int a, int b)
{
if (b == 0)
{
throw new Exception("Cannot divide by zero");
}
return a / b;
}
try
{
var result = Divide(4, 2);
Console.WriteLine(result * 2); // 4
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return;
}Turns into this π
public ErrorOr<float> Divide(int a, int b)
{
if (b == 0)
{
return Error.Unexpected(description: "Cannot divide by zero");
}
return a / b;
}
var result = Divide(4, 2);
if (result.IsError)
{
Console.WriteLine(result.FirstError.Description);
return;
}
Console.WriteLine(result.Value * 2); // 4Or, using Then/Else and Switch/Match, you can do this π
Divide(4, 2)
.Then(val => val * 2)
.SwitchFirst(
onValue: Console.WriteLine, // 4
onFirstError: error => Console.WriteLine(error.Description));Internally, the ErrorOr object has a list of Errors, so if you have multiple errors, you don't need to compromise and have only the first one.
public class User(string _name)
{
public static ErrorOr<User> Create(string name)
{
List<Error> errors = [];
if (name.Length < 2)
{
errors.Add(Error.Validation(description: "Name is too short"));
}
if (name.Length > 100)
{
errors.Add(Error.Validation(description: "Name is too long"));
}
if (string.IsNullOrWhiteSpace(name))
{
errors.Add(Error.Validation(description: "Name cannot be empty or whitespace only"));
}
if (errors.Count > 0)
{
return errors;
}
return new User(name);
}
}The ErrorOr object has a variety of methods that allow you to work with it in a functional way.
This allows you to chain methods together, and handle the result in a clean and concise way.
return await _userRepository.GetByIdAsync(id)
.Then(user => user.IncrementAge()
.Then(success => user)
.Else(errors => Error.Unexpected("Not expected to fail")))
.FailIf(user => !user.IsOverAge(18), UserErrors.UnderAge)
.ThenDo(user => _logger.LogInformation($"User {user.Id} incremented age to {user.Age}"))
.ThenAsync(user => _userRepository.UpdateAsync(user))
.Match(
_ => NoContent(),
errors => errors.ToActionResult());ErrorOr<string> foo = await "2".ToErrorOr()
.Then(int.Parse) // 2
.FailIf(val => val > 2, Error.Validation(description: $"{val} is too big") // 2
.ThenDoAsync(Task.Delay) // Sleep for 2 milliseconds
.ThenDo(val => Console.WriteLine($"Finished waiting {val} milliseconds.")) // Finished waiting 2 milliseconds.
.ThenAsync(val => Task.FromResult(val * 2)) // 4
.Then(val => $"The result is {val}") // "The result is 4"
.Else(errors => Error.Unexpected(description: "Yikes")) // "The result is 4"
.MatchFirst(
value => value, // "The result is 4"
firstError => $"An error occurred: {firstError.Description}");ErrorOr<string> foo = await "5".ToErrorOr()
.Then(int.Parse) // 5
.FailIf(val => val > 2, Error.Validation(description: $"{val} is too big") // Error.Validation()
.ThenDoAsync(Task.Delay) // Error.Validation()
.ThenDo(val => Console.WriteLine($"Finished waiting {val} milliseconds.")) // Error.Validation()
.ThenAsync(val => Task.FromResult(val * 2)) // Error.Validation()
.Then(val => $"The result is {val}") // Error.Validation()
.Else(errors => Error.Unexpected(description: "Yikes")) // Error.Unexpected()
.MatchFirst(
value => value,
firstError => $"An error occurred: {firstError.Description}"); // An error occurred: YikesThere are implicit converters from TResult, Error, List<Error> to ErrorOr<TResult>
ErrorOr<int> result = 5;
ErrorOr<int> result = Error.Unexpected();
ErrorOr<int> result = [Error.Validation(), Error.Validation()];public ErrorOr<int> IntToErrorOr()
{
return 5;
}public ErrorOr<int> SingleErrorToErrorOr()
{
return Error.Unexpected();
}public ErrorOr<int> MultipleErrorsToErrorOr()
{
return [
Error.Validation(description: "Invalid Name"),
Error.Validation(description: "Invalid Last Name")
];
}ErrorOr<int> result = ErrorOrFactory.From(5);
ErrorOr<int> result = ErrorOrFactory.From<int>(Error.Unexpected());
ErrorOr<int> result = ErrorOrFactory.From<int>([Error.Validation(), Error.Validation()]);public ErrorOr<int> GetValue()
{
return ErrorOrFactory.From(5);
}public ErrorOr<int> SingleErrorToErrorOr()
{
return ErrorOrFactory.From<int>(Error.Unexpected());
}public ErrorOr<int> MultipleErrorsToErrorOr()
{
return ErrorOrFactory.From([
Error.Validation(description: "Invalid Name"),
Error.Validation(description: "Invalid Last Name")
]);
}ErrorOr<int> result = 5.ToErrorOr();
ErrorOr<int> result = Error.Unexpected().ToErrorOr<int>();
ErrorOr<int> result = new[] { Error.Validation(), Error.Validation() }.ToErrorOr<int>();ErrorOr<int> result = User.Create();
if (result.IsError)
{
// the result contains one or more errors
}ErrorOr<int> result = User.Create();
if (!result.IsError) // the result contains a value
{
Console.WriteLine(result.Value);
}ErrorOr<int> result = User.Create();
if (result.IsError)
{
result.Errors // contains the list of errors that occurred
.ForEach(error => Console.WriteLine(error.Description));
}ErrorOr<int> result = User.Create();
if (result.IsError)
{
var firstError = result.FirstError; // only the first error that occurred
Console.WriteLine(firstError == result.Errors[0]); // true
}ErrorOr<int> result = User.Create();
if (result.IsError)
{
result.ErrorsOrEmptyList // List<Error> { /* one or more errors */ }
return;
}
result.ErrorsOrEmptyList // List<Error> { }Safely extract the value without risking an exception:
if (result.TryGetValue(out var user))
{
Console.WriteLine($"Got user: {user.Name}");
}
else
{
Console.WriteLine("Result was an error");
}Safely extract errors without risking an exception:
if (result.TryGetErrors(out var errors))
{
foreach (var error in errors)
{
Console.WriteLine($"Error: {error.Code}");
}
}Get the value or a fallback if in error state:
User user = result.GetValueOrDefault(User.Anonymous);
int count = result.GetValueOrDefault(); // returns default(int) = 0 if errorThe Match method receives two functions, onValue and onError, onValue will be invoked if the result is success, and onError is invoked if the result is an error.
string foo = result.Match(
value => value,
errors => $"{errors.Count} errors occurred.");string foo = await result.MatchAsync(
value => Task.FromResult(value),
errors => Task.FromResult($"{errors.Count} errors occurred."));The MatchFirst method receives two functions, onValue and onError, onValue will be invoked if the result is success, and onError is invoked if the result is an error.
Unlike Match, if the state is error, MatchFirst's onError function receives only the first error that occurred, not the entire list of errors.
string foo = result.MatchFirst(
value => value,
firstError => firstError.Description);string foo = await result.MatchFirstAsync(
value => Task.FromResult(value),
firstError => Task.FromResult(firstError.Description));The Switch method receives two actions, onValue and onError, onValue will be invoked if the result is success, and onError is invoked if the result is an error.
result.Switch(
value => Console.WriteLine(value),
errors => Console.WriteLine($"{errors.Count} errors occurred."));await result.SwitchAsync(
value => { Console.WriteLine(value); return Task.CompletedTask; },
errors => { Console.WriteLine($"{errors.Count} errors occurred."); return Task.CompletedTask; });The SwitchFirst method receives two actions, onValue and onError, onValue will be invoked if the result is success, and onError is invoked if the result is an error.
Unlike Switch, if the state is error, SwitchFirst's onError function receives only the first error that occurred, not the entire list of errors.
result.SwitchFirst(
value => Console.WriteLine(value),
firstError => Console.WriteLine(firstError.Description));await result.SwitchFirstAsync(
value => { Console.WriteLine(value); return Task.CompletedTask; },
firstError => { Console.WriteLine(firstError.Description); return Task.CompletedTask; });Then receives a function, and invokes it only if the result is not an error.
ErrorOr<int> foo = result
.Then(val => val * 2);Multiple Then methods can be chained together.
ErrorOr<string> foo = result
.Then(val => val * 2)
.Then(val => $"The result is {val}");If any of the methods return an error, the chain will break and the errors will be returned.
ErrorOr<int> Foo() => Error.Unexpected();
ErrorOr<string> foo = result
.Then(val => val * 2)
.Then(_ => GetAnError())
.Then(val => $"The result is {val}") // this function will not be invoked
.Then(val => $"The result is {val}"); // this function will not be invokedThenAsync receives an asynchronous function, and invokes it only if the result is not an error.
ErrorOr<string> foo = await result
.ThenAsync(val => DoSomethingAsync(val))
.ThenAsync(val => DoSomethingElseAsync($"The result is {val}"));ThenDo and ThenDoAsync are similar to Then and ThenAsync, but instead of invoking a function that returns a value, they invoke an action.
ErrorOr<string> foo = result
.ThenDo(val => Console.WriteLine(val))
.ThenDo(val => Console.WriteLine($"The result is {val}"));ErrorOr<string> foo = await result
.ThenDoAsync(val => Task.Delay(val))
.ThenDo(val => Console.WriteLine($"Finsihed waiting {val} seconds."))
.ThenDoAsync(val => Task.FromResult(val * 2))
.ThenDo(val => $"The result is {val}");You can mix and match Then, ThenDo, ThenAsync, ThenDoAsync methods.
ErrorOr<string> foo = await result
.ThenDoAsync(val => Task.Delay(val))
.Then(val => val * 2)
.ThenAsync(val => DoSomethingAsync(val))
.ThenDo(val => Console.WriteLine($"Finsihed waiting {val} seconds."))
.ThenAsync(val => Task.FromResult(val * 2))
.Then(val => $"The result is {val}");FailIf receives a predicate and an error. If the predicate is true, FailIf will return the error. Otherwise, it will return the value of the result.
ErrorOr<int> foo = result
.FailIf(val => val > 2, Error.Validation(description: $"{val} is too big"));Once an error is returned, the chain will break and the error will be returned.
var result = "2".ToErrorOr()
.Then(int.Parse) // 2
.FailIf(val => val > 1, Error.Validation(description: $"{val} is too big") // validation error
.Then(num => num * 2) // this function will not be invoked
.Then(num => num * 2) // this function will not be invokedElse receives a value or a function. If the result is an error, Else will return the value or invoke the function. Otherwise, it will return the value of the result.
ErrorOr<string> foo = result
.Else("fallback value");ErrorOr<string> foo = result
.Else(errors => $"{errors.Count} errors occurred.");ErrorOr<string> foo = await result
.ElseAsync(Task.FromResult("fallback value"));ErrorOr<string> foo = await result
.ElseAsync(errors => Task.FromResult($"{errors.Count} errors occurred."));You can mix Then, FailIf, Else, Switch and Match methods together.
ErrorOr<string> foo = await result
.ThenDoAsync(val => Task.Delay(val))
.FailIf(val => val > 2, Error.Validation(description: $"{val} is too big"))
.ThenDo(val => Console.WriteLine($"Finished waiting {val} seconds."))
.ThenAsync(val => Task.FromResult(val * 2))
.Then(val => $"The result is {val}")
.Else(errors => Error.Unexpected())
.MatchFirst(
value => value,
firstError => $"An error occurred: {firstError.Description}");Each Error instance has a Type property, which is an enum value that represents the type of the error.
The following error types are built in:
public enum ErrorType
{
Failure,
Unexpected,
Validation,
Conflict,
NotFound,
Unauthorized,
Forbidden,
BadRequest,
Timeout,
Gone,
Locked,
TooManyRequests,
Unavailable,
}Each error type has a static method that creates an error of that type. For example:
var error = Error.NotFound();optionally, you can pass a code, description and metadata to the error:
var error = Error.Unexpected(
code: "User.ShouldNeverHappen",
description: "A user error that should never happen",
metadata: new Dictionary<string, object>
{
{ "user", user },
});The ErrorType enum is a good way to categorize errors.
You can create your own error types if you would like to categorize your errors differently.
A custom error type can be created with the Custom static method
public static class MyErrorTypes
{
const int ShouldNeverHappen = 12;
}
var error = Error.Custom(
type: MyErrorTypes.ShouldNeverHappen,
code: "User.ShouldNeverHappen",
description: "A user error that should never happen");You can use the Error.NumericType method to retrieve the numeric type of the error.
var errorMessage = Error.NumericType switch
{
MyErrorType.ShouldNeverHappen => "Consider replacing dev team",
_ => "An unknown error occurred.",
};There are a few built in result types:
ErrorOr<Success> result = Result.Success;
ErrorOr<Created> result = Result.Created;
ErrorOr<Updated> result = Result.Updated;
ErrorOr<Deleted> result = Result.Deleted;Which can be used as following
ErrorOr<Deleted> DeleteUser(Guid id)
{
var user = await _userRepository.GetByIdAsync(id);
if (user is null)
{
return Error.NotFound(description: "User not found.");
}
await _userRepository.DeleteAsync(user);
return Result.Deleted;
}MapError transforms errors without affecting the value path. Useful for error enrichment or translation.
result.MapError(error => Error.Validation(
code: $"API.{error.Code}",
description: error.Description,
metadata: new Dictionary<string, object> { { "OriginalType", error.Type } }));Transform the entire error list:
result.MapErrors(errors => errors
.Where(e => e.Type == ErrorType.Validation)
.ToList());Unlike Else which always recovers to a value, OrElse chains recovery attempts that might also fail.
// Try primary, then fallback to cache, then to default
primarySource.GetUser(id)
.OrElse(_ => cache.GetUser(id))
.OrElse(_ => User.CreateDefault());Safely extract the value or throw with a descriptive error message:
var user = result.GetValueOrThrow(); // Throws with error codes if failed
var user = result.GetValueOrThrow("User must be present for this operation");Handle nested ErrorOr<ErrorOr<T>> scenarios:
ErrorOr<ErrorOr<User>> nested = GetNestedResult();
ErrorOr<User> flattened = nested.Flatten();Deconstruct ErrorOr into its components:
var (value, errors) = result;
if (errors is not null)
{
// handle errors
}
else
{
// use value
}
// Or with three parameters
var (isError, value, errors) = result;Compare ErrorOr instances without considering error order:
var comparer = ErrorOrUnorderedEqualityComparer<int>.Instance;
var areEqual = comparer.Equals(result1, result2); // true if same errors in any orderTry executes a function and wraps any thrown exception as an Error, allowing safe interop with exception-throwing code.
// Basic usage - wraps exceptions as Unexpected errors
ErrorOr<int> result = ErrorOr<int>.Try(() => int.Parse("not a number"));
// result.FirstError.Code == "FormatException"
// result.FirstError.Description == "The input string 'not a number' was not in a correct format."// With custom error mapping
ErrorOr<User> result = ErrorOr<User>.Try(
() => repository.GetUser(id),
ex => Error.Failure("Database.Error", ex.Message));ErrorOr<Data> result = await ErrorOr<Data>.TryAsync(
async () => await httpClient.GetFromJsonAsync<Data>(url));// With async error mapping
ErrorOr<Data> result = await ErrorOr<Data>.TryAsync(
async () => await httpClient.GetFromJsonAsync<Data>(url),
async ex => await LogAndCreateError(ex));Combine aggregates multiple ErrorOr results, returning a tuple of all values if successful, or all accumulated errors if any failed.
var nameResult = ValidateName(name);
var emailResult = ValidateEmail(email);
var ageResult = ValidateAge(age);
// Combine up to 8 results (returns tuple on success)
var combined = ErrorOrCombine.Combine(nameResult, emailResult, ageResult);
combined.Match(
tuple => CreateUser(tuple.First, tuple.Second, tuple.Third),
errors => HandleAllErrors(errors)); // All errors from all failed validationsCollect aggregates a sequence of ErrorOr<T> results into a single ErrorOr<List<T>>:
var userIds = new[] { 1, 2, 3, 4, 5 };
var userResults = userIds.Select(id => GetUser(id)); // IEnumerable<ErrorOr<User>>
ErrorOr<List<User>> allUsers = ErrorOrCombine.Collect(userResults);
// Returns List<User> if all succeeded, or all accumulated errorsSelect is an alias for Then, providing LINQ query syntax compatibility:
ErrorOr<string> result = errorOr.Select(value => value.ToString());
// Async variant
ErrorOr<Data> result = await errorOr.SelectAsync(async value => await TransformAsync(value));SelectMany chains operations that return ErrorOr, enabling LINQ query syntax:
ErrorOr<Order> result = errorOr.SelectMany(user => GetOrderForUser(user.Id));
// Can be used with LINQ query syntax
var result =
from user in GetUser(userId)
from order in GetOrder(user.DefaultOrderId)
from item in GetFirstItem(order.Id)
select item;Where filters values based on a predicate, returning an error if the predicate fails:
ErrorOr<int> result = errorOr
.Where(value => value > 0, Error.Validation("Value.NonPositive", "Value must be positive"));
// With error factory (access to the value)
ErrorOr<User> result = errorOr
.Where(
user => user.IsActive,
user => Error.Validation("User.Inactive", $"User {user.Id} is not active"));
// Async predicate
ErrorOr<User> result = await errorOr
.WhereAsync(
async user => await IsUserAuthorizedAsync(user),
Error.Unauthorized());A nice approach, is creating a static class with the expected errors. For example:
public static partial class DivisionErrors
{
public static Error CannotDivideByZero = Error.Unexpected(
code: "Division.CannotDivideByZero",
description: "Cannot divide by zero.");
}Which can later be used as following π
public ErrorOr<float> Divide(int a, int b)
{
if (b == 0)
{
return DivisionErrors.CannotDivideByZero;
}
return a / b;
}VsaResults includes a feature pipeline designed for Vertical Slice Architecture. Features encapsulate a complete vertical slice of functionality with built-in validation, execution, and observability.
| Interface | Pipeline Stages | Use Case |
|---|---|---|
IQueryFeature<TRequest, TResult> |
Validate β Execute Query | Read-only operations |
IMutationFeature<TRequest, TResult> |
Validate β Enforce Requirements β Execute Mutation β Run Side Effects | State-changing operations |
public static class GetUserById
{
public record Request(Guid Id);
public class Feature(
IFeatureValidator<Request> validator,
IFeatureQuery<Request, User> query)
: IQueryFeature<Request, User>
{
public IFeatureValidator<Request> Validator => validator;
public IFeatureQuery<Request, User> Query => query;
}
public class Validator : IFeatureValidator<Request>
{
public Task<ErrorOr<Request>> ValidateAsync(Request request, CancellationToken ct = default) =>
request.Id == Guid.Empty
? Task.FromResult<ErrorOr<Request>>(Error.Validation("User.InvalidId", "User ID cannot be empty."))
: Task.FromResult<ErrorOr<Request>>(request);
}
public class Query(IUserRepository repository) : IFeatureQuery<Request, User>
{
public Task<ErrorOr<User>> ExecuteAsync(Request request, CancellationToken ct = default) =>
Task.FromResult(repository.GetById(request.Id));
}
}
// Execute the feature
var feature = new GetUserById.Feature(new GetUserById.Validator(), new GetUserById.Query(repo));
var result = await feature.ExecuteAsync(new GetUserById.Request(userId));public static class CreateUser
{
public record Request(string Email, string Name);
public class Feature(
IFeatureValidator<Request> validator,
IFeatureMutator<Request, User> mutator,
IFeatureSideEffects<Request>? sideEffects = null)
: IMutationFeature<Request, User>
{
public IFeatureValidator<Request> Validator => validator;
public IFeatureMutator<Request, User> Mutator => mutator;
public IFeatureSideEffects<Request> SideEffects => sideEffects ?? NoOpSideEffects<Request>.Instance;
}
public class Validator : IFeatureValidator<Request>
{
public Task<ErrorOr<Request>> ValidateAsync(Request request, CancellationToken ct = default)
{
var errors = new List<Error>();
if (string.IsNullOrWhiteSpace(request.Email))
errors.Add(Error.Validation("User.InvalidEmail", "Email is required."));
if (string.IsNullOrWhiteSpace(request.Name))
errors.Add(Error.Validation("User.InvalidName", "Name is required."));
return errors.Count > 0
? Task.FromResult<ErrorOr<Request>>(errors)
: Task.FromResult<ErrorOr<Request>>(request);
}
}
public class Mutator(IUserRepository repository) : IFeatureMutator<Request, User>
{
public async Task<ErrorOr<User>> ExecuteAsync(FeatureContext<Request> context, CancellationToken ct = default)
{
var request = context.Request;
if (repository.ExistsByEmail(request.Email))
return Error.Conflict("User.DuplicateEmail", $"Email {request.Email} is already registered.");
var user = new User(Guid.NewGuid(), request.Email, request.Name, DateTime.UtcNow);
repository.Add(user);
// Add context for wide event logging
context.AddContext("user_id", user.Id);
return user;
}
}
}| Component | Interface | Purpose |
|---|---|---|
| Validator | IFeatureValidator<TRequest> |
Validates the incoming request |
| Requirements | IFeatureRequirements<TRequest> |
Loads entities and enforces business rules (mutations only) |
| Mutator | IFeatureMutator<TRequest, TResult> |
Executes the core mutation logic |
| Query | IFeatureQuery<TRequest, TResult> |
Executes read-only queries |
| Side Effects | IFeatureSideEffects<TRequest> |
Runs post-success effects like notifications (mutations only) |
Each component has a no-op default (NoOpValidator, NoOpRequirements, NoOpSideEffects) so you only implement what you need.
Wide Events (also known as Canonical Log Lines) capture a single comprehensive structured log entry per feature execution. Instead of scattered log lines, you get one event with full context for debugging and observability.
- One event per execution - Not scattered log lines throughout the code
- High cardinality fields -
user_id,trace_id,feature_namefor precise filtering - High dimensionality - Many fields for rich querying in your observability stack
- Build throughout, emit once - Context accumulates during execution, emitted at the end
The WideEvent includes:
| Category | Fields |
|---|---|
| Trace Context | TraceId, SpanId, ParentSpanId |
| Feature Context | FeatureName, FeatureType, RequestType, ResultType |
| Service Context | ServiceName, ServiceVersion, Environment, Region, Host |
| Pipeline Metadata | ValidatorType, RequirementsType, MutatorType, SideEffectsType |
| Timing | ValidationMs, RequirementsMs, ExecutionMs, SideEffectsMs, TotalMs |
| Outcome | Outcome (success, validation_failure, execution_failure, etc.) |
| Error Context | ErrorCode, ErrorType, ErrorMessage, FailedAtStage |
| Business Context | Context dictionary with custom fields |
// Register wide events in DI
builder.Services.AddWideEvents();
// Execute with wide event emission (emitter injected automatically via FeatureHandler/FeatureController)
var result = await feature.ExecuteAsync(request, emitter);Implement IWideEventEmitter to integrate with your telemetry system:
public class CustomWideEventSink : IWideEventSink
{
public ValueTask WriteAsync(WideEvent wideEvent, CancellationToken ct = default)
{
// Write to OpenTelemetry, Datadog, Honeycomb, etc.
return ValueTask.CompletedTask;
}
}Add custom fields to the wide event during execution:
public class Mutator : IFeatureMutator<Request, User>
{
public async Task<ErrorOr<User>> ExecuteAsync(FeatureContext<Request> context, CancellationToken ct)
{
// ... create user ...
// These fields appear in RequestContext
context.AddContext("user_id", user.Id);
context.AddContext("user_role", user.Role.ToString());
context.AddContext("welcome_email_sent", true);
return user;
}
}{
"FeatureName": "CreateUser",
"FeatureType": "Mutation",
"Outcome": "success",
"TotalMs": 45.23,
"ValidationMs": 1.2,
"RequirementsMs": 0,
"ExecutionMs": 43.8,
"SideEffectsMs": 0.23,
"TraceId": "abc123",
"RequestContext": {
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"user_role": "Admin"
}
}A complete sample Web API is available in samples/VsaResults.Sample.WebApi demonstrating:
- Query Features:
GetUserById,GetAllUsers,GetAllProducts - Mutation Features:
CreateUser,DeleteUser,ReserveStock - ASP.NET Core Integration: Minimal APIs and MVC controllers
- Wide Event Emission: Serilog integration
- Messaging: MassTransit consumers with message-wide events
Run the sample:
cd samples/VsaResults.Sample.WebApi
dotnet runThe NuGet packages include XML documentation. Generate a browsable reference site with tools like DocFX if you need hosted API docs.
If you have any questions, comments, or suggestions, please open an issue or create a pull request π
- Amichai Mantinband - Creator of the original ErrorOr library that this project is based on
- OneOf - An awesome library which provides F# style discriminated unions behavior for C#
This project is licensed under the terms of the MIT license.