Skip to content

Repository files navigation

ManagedCode.Communication

Result pattern for .NET that replaces exceptions with type-safe return values. Features railway-oriented programming, ASP.NET Core integration, RFC 7807 Problem Details, and built-in pagination. Designed for production systems requiring explicit error handling without the overhead of throwing exceptions.

NuGetLicense: MIT.NET

Table of Contents

Overview

ManagedCode.Communication brings functional error handling to .NET through the Result pattern. Instead of throwing exceptions, methods return Result types that explicitly indicate success or failure. This approach eliminates hidden control flow, improves performance, and makes error handling a first-class concern in your codebase.

Why Result Pattern?

Traditional exception handling has several drawbacks:

  • Performance overhead: Throwing exceptions is expensive
  • Hidden control flow: Exceptions create invisible exit points in your code
  • Unclear contracts: Methods don't explicitly declare what errors they might produce
  • Testing complexity: Exception paths require separate test scenarios

The Result pattern solves these issues by:

  • Explicit error handling: Errors are part of the method signature
  • Performance: No exception throwing overhead
  • Composability: Chain operations using railway-oriented programming
  • Type safety: Compiler ensures error handling
  • Testability: All paths are explicit and easy to test

Key Features

🎯 Core Result Types

  • Result: Represents success/failure without a value
  • Result<T>: Represents success with value T or failure
  • CollectionResult<T>: Represents collections with built-in pagination
  • Problem: RFC 7807 compliant error details

⚙️ Static Factory Abstractions

  • Leverage C# static interface members to centralize factory overloads for every result, command, and collection type.
  • IResultFactory<T> and ICommandFactory<T> deliver a consistent surface while bridge helpers remove repetitive boilerplate.
  • Extending the library now only requires implementing the minimal Succeed/Fail contract—the shared helpers provide the rest.

🧭 Pagination Utilities

  • PaginationRequest encapsulates skip/take semantics, built-in normalization, and clamping helpers.
  • PaginationOptions lets you define default, minimum, and maximum page sizes for a bounded API surface.
  • PaginationCommand captures pagination intent as a first-class command with generated overloads for skip/take, page numbers, and enum command types.
  • CollectionResult<T>.Succeed(..., PaginationRequest request, int totalItems) keeps result metadata aligned with pagination commands.

🚂 Railway-Oriented Programming

Complete set of functional combinators for composing operations:

  • Map: Transform success values
  • Bind / Then: Chain Result-returning operations
  • Tap / Do: Execute side effects
  • Match: Pattern matching on success/failure
  • Compensate: Recovery from failures
  • Merge / Combine: Aggregate multiple results

🌐 Framework Integration

  • ASP.NET Core: Automatic HTTP response mapping
  • SignalR: Hub filters for real-time error handling
  • Microsoft Orleans: Grain call filters and surrogates
  • Command Pattern: Built-in command infrastructure with idempotency

🔍 Observability Built In

  • Source-generated LoggerCenter APIs provide zero-allocation logging across ASP.NET Core filters, SignalR hubs, and command stores.
  • Call sites automatically check log levels, so you only pay for the logs you emit.
  • Extend logging with additional [LoggerMessage] partials to keep high-volume paths allocation free.

🛡️ Error Types

Pre-defined error categories with appropriate HTTP status codes:

  • Validation errors (400 Bad Request)
  • Not Found (404)
  • Unauthorized (401)
  • Forbidden (403)
  • Internal Server Error (500)
  • Custom enum-based errors

Installation

Package Manager Console

# Core libraryInstall-Package ManagedCode.Communication
# ASP.NET Core integrationInstall-Package ManagedCode.Communication.AspNetCore
# Minimal API extensionsInstall-Package ManagedCode.Communication.Extensions
# Orleans integrationInstall-Package ManagedCode.Communication.Orleans

.NET CLI

# Core library
dotnet add package ManagedCode.Communication
# ASP.NET Core integration
dotnet add package ManagedCode.Communication.AspNetCore
# Minimal API extensions
dotnet add package ManagedCode.Communication.Extensions
# Orleans integration
dotnet add package ManagedCode.Communication.Orleans

PackageReference

<PackageReferenceInclude="ManagedCode.Communication"Version="9.6.0" />
<PackageReferenceInclude="ManagedCode.Communication.AspNetCore"Version="9.6.0" />
<PackageReferenceInclude="ManagedCode.Communication.Extensions"Version="9.6.0" />
<PackageReferenceInclude="ManagedCode.Communication.Orleans"Version="9.6.0" />

Logging Configuration

The library includes integrated logging for error scenarios. Configure logging to capture detailed error information:

ASP.NET Core Setup

varbuilder=WebApplication.CreateBuilder(args);// Add your logging configurationbuilder.Logging.AddConsole();builder.Logging.AddDebug();// Register other servicesbuilder.Services.AddControllers();// Configure Communication library - this enables automatic error loggingbuilder.Services.ConfigureCommunication();varapp=builder.Build();

Minimal API Result Mapping

Add the optional ManagedCode.Communication.Extensions package to bridge Minimal API endpoints with the Result pattern. The package provides the ResultEndpointFilter and a fluent helper WithCommunicationResults that wraps the endpoint builder and returns IResult instances automatically:

varbuilder=WebApplication.CreateBuilder(args);builder.Services.ConfigureCommunication();varapp=builder.Build();// Apply the filter to a single endpointapp.MapGet("/orders/{id}",async(Guidid,IOrderServiceorders)=>awaitorders.GetAsync(id)).WithCommunicationResults();// Or apply it to a group so every route inherits the conversionapp.MapGroup("/orders").WithCommunicationResults().MapPost(string.Empty,async(CreateOrdercommand,IOrderServiceorders)=>awaitorders.CreateAsync(command));app.Run();

Handlers can return any Result or Result<T> instance and the filter will reuse the existing ASP.NET Core converters so you do not need to write manual IResult translations.

Resilient HTTP Clients

The extensions package also ships helpers that turn HttpClient calls directly into Result instances and optionally run them through Polly resilience pipelines:

usingManagedCode.Communication.Extensions.Http;usingPolly;usingPolly.Retry;varpipeline=newResiliencePipelineBuilder<HttpResponseMessage>().AddRetry(newRetryStrategyOptions<HttpResponseMessage>{MaxRetryAttempts=3,Delay=TimeSpan.FromMilliseconds(200),ShouldHandle=newPredicateBuilder<HttpResponseMessage>().HandleResult(response =>!response.IsSuccessStatusCode)}).Build();varresult=awaithttpClient.SendForResultAsync<OrderDto>(()=>newHttpRequestMessage(HttpMethod.Get,$"/orders/{orderId}"),pipeline);if(result.IsSuccess){// access result.Value without manually reading the HTTP payload}

The helpers use the existing HttpResponseMessage converters, so non-success status codes automatically map to a Problem with the response body and status code. success responses map to 200 OK/204 No Content while failures become RFC 7807 problem details. Native Microsoft.AspNetCore.Http.IResult responses pass through unchanged, so you can mix and match traditional Minimal API patterns with ManagedCode.Communication results.

Console Application Setup

varservices=newServiceCollection();// Add loggingservices.AddLogging(builder =>{builder.AddConsole().SetMinimumLevel(LogLevel.Information);});// Configure Communication libraryservices.ConfigureCommunication();varserviceProvider=services.BuildServiceProvider();

The library automatically logs errors in Result factory methods (From, Try, etc.) with detailed context including file names, line numbers, and method names for easier debugging.

Core Concepts

Result Type

The Result type represents an operation that can either succeed or fail:

publicstructResult{publicboolIsSuccess{get;}publicProblem?Problem{get;}}

Result Type with Value

The generic Result<T> includes a value on success:

publicstructResult<T>{publicboolIsSuccess{get;}publicT?Value{get;}publicProblem?Problem{get;}}

Problem Type

Implements RFC 7807 Problem Details for HTTP APIs:

publicclassProblem{publicstringType{get;set;}publicstringTitle{get;set;}publicintStatusCode{get;set;}publicstringDetail{get;set;}publicDictionary<string,object>Extensions{get;set;}}

Display Message Helpers

Use built-in helpers to convert technical Problem payloads into UI-friendly messages:

varproblem=Problem.Create("RegistrationUnavailable","Service is temporarily unavailable",503);problem.ErrorCode="RegistrationUnavailable";// Default message resolution chain:// ErrorCode mapper -> Detail -> Title -> defaultMessage -> "An error occurred"varmessage=problem.ToDisplayMessage(defaultMessage:"Please try again later");varregistrationMessages=newDictionary<string,string>{["RegistrationUnavailable"]="Registration is currently unavailable.",["RegistrationBlocked"]="Registration is temporarily blocked.",["RegistrationInviteRequired"]="Registration requires an invitation code."};// 1) Dictionary overloadvarbyDictionary=problem.ToDisplayMessage(registrationMessages,defaultMessage:"Please try again later");// 2) Tuple mappings overloadvarbyTuples=problem.ToDisplayMessage("Please try again later",("RegistrationUnavailable","Registration is currently unavailable."),("RegistrationBlocked","Registration is temporarily blocked."),("RegistrationInviteRequired","Registration requires an invitation code."));// 3) Delegate overloadstaticstring?ResolveRegistrationMessage(stringcode)=>codeswitch{"RegistrationUnavailable"=>"Registration is currently unavailable.","RegistrationBlocked"=>"Registration is temporarily blocked.","RegistrationInviteRequired"=>"Registration requires an invitation code.",
_ =>null};varbyDelegate=problem.ToDisplayMessage(ResolveRegistrationMessage,defaultMessage:"Please try again later");// The same overloads are available for Result, Result<T> and CollectionResult<T>varresultMessage=Result.Fail(problem).ToDisplayMessage(registrationMessages,defaultMessage:"Please try again later");// Typed extension accessif(problem.TryGetExtension("retryAfter",outintretryAfterSeconds)){Console.WriteLine($"Retry after: {retryAfterSeconds}s");}

Quick Start

Basic Usage

usingManagedCode.Communication;// Creating Resultsvarsuccess=Result.Succeed();varfailure=Result.Fail("Operation failed");// Results with valuesvaruserResult=Result<User>.Succeed(newUser{Id=1,Name="John"});varnotFound=Result<User>.FailNotFound("User not found");// Validation errorsvarinvalid=Result.FailValidation(("email","Email is required"),("age","Age must be positive"));// From exceptionstry{// risky operation}catch(Exceptionex){varerror=Result.Fail(ex);}

Checking Result State

if(result.IsSuccess){// Handle success}if(result.IsFailed){// Handle failure}if(result.IsInvalid){// Handle validation errors}// Pattern matchingresult.Match(onSuccess:()=>Console.WriteLine("Success!"),onFailure: problem =>Console.WriteLine($"Failed: {problem.Detail}"));

API Reference

Result Creation Methods

Success Methods

// Basic successResult.Succeed()Result<T>.Succeed(Tvalue)CollectionResult<T>.Succeed(T[]items,intpageNumber,intpageSize,inttotalItems)// From operationsResult.From(Actionaction)Result<T>.From(Func<T>func)Result<T>.From(Task<T>task)// Try pattern with exception catchingResult.Try(Actionaction)Result<T>.Try(Func<T>func)

Failure Methods

// Basic failuresResult.Fail()
Result.Fail(stringtitle)
Result.Fail(stringtitle,stringdetail)
Result.Fail(Problemproblem)
Result.Fail(Exceptionexception)// HTTP status failures
Result.FailNotFound(stringdetail)
Result.FailUnauthorized(stringdetail)
Result.FailForbidden(stringdetail)// Validation failures
Result.FailValidation(params(stringfield,stringmessage)[]errors)Result.Invalid(stringmessage)Result.Invalid(stringfield,stringmessage)// Enum-based failuresResult.Fail<TEnum>(TEnumerrorCode)where TEnum : Enum

Transformation Methods

// Map: Transform the valueResult<int>ageResult=userResult.Map(user =>user.Age);// Bind: Chain operations that return ResultsResult<Order>orderResult=userResult.Bind(user =>GetUserCart(user.Id)).Bind(cart =>CreateOrder(cart));// Tap: Execute side effectsResult<User>result=userResult.Tap(user =>_logger.LogInfo($"Processing user {user.Id}")).Tap(user =>_cache.Set(user.Id,user));

Validation Methods

// Ensure: Add validationResult<User>validUser=userResult.Ensure(user =>user.Age>=18,Problem.Create("User must be 18+")).Ensure(user =>user.Email.Contains("@"),Problem.Create("Invalid email"));// Where: Filter with predicateResult<User>filtered=userResult.Where(user =>user.IsActive,"User is not active");// FailIf: Conditional failureResult<Order>order=orderResult.FailIf(o =>o.Total<=0,"Order total must be positive");// OkIf: Must satisfy conditionResult<Payment>payment=paymentResult.OkIf(p =>p.IsAuthorized,"Payment not authorized");

Railway-Oriented Programming

Railway-oriented programming treats operations as a series of tracks where success continues on the main track and failures switch to an error track:

Basic Chaining

publicResult<Order>ProcessOrder(intuserId){returnResult.From(()=>GetUser(userId)).Then(user =>ValidateUser(user)).Then(user =>GetUserCart(user.Id)).Then(cart =>ValidateCart(cart)).Then(cart =>CreateOrder(cart)).Then(order =>ProcessPayment(order)).Then(order =>SendConfirmation(order));}

Async Operations

publicasyncTask<Result<Order>>ProcessOrderAsync(intuserId){returnawaitResult.From(()=>GetUserAsync(userId)).ThenAsync(user =>ValidateUserAsync(user)).ThenAsync(user =>GetUserCartAsync(user.Id)).ThenAsync(cart =>CreateOrderAsync(cart)).ThenAsync(order =>ProcessPaymentAsync(order)).ThenAsync(order =>SendConfirmationAsync(order));}

Error Recovery

varresult=awaitGetPrimaryService().CompensateAsync(async error =>{_logger.LogWarning($"Primary service failed: {error.Detail}");returnawaitGetFallbackService();}).CompensateWith(defaultValue);// Final fallback

Combining Multiple Results

// Merge: Stop at first failurevarfirstFailureResult=Result.Merge(ValidateName(name),ValidateEmail(email),ValidateAge(age));// MergeAll: aggregate all failuresvarallFailuresResult=Result.MergeAll(ValidateName(name),ValidateEmail(email),ValidateAge(age));if(allFailuresResult.TryGetProblem(outvarproblem)){// All failures were validation failures:// problem.GetValidationErrors() returns merged field errors.//// Mixed failures (401/403/500/...) return aggregate problem:// problem.StatusCode == 500// problem.Extensions["errors"] contains the original Problem[] list.}if(allFailuresResult.TryGetProblem(outvaraggregateProblem)&&aggregateProblem.TryGetExtension("errors",outProblem[]?originalErrors)){foreach(varerrorinoriginalErrors){Console.WriteLine($"{error.StatusCode}: {error.Title} - {error.Detail}");}}// Combine: Aggregate valuesvarcombined=Result.Combine(GetUserProfile(),GetUserSettings(),GetUserPermissions());// Returns CollectionResult<T>// CombineAll: aggregate failures while preserving original errorsvarcombinedAll=Result.CombineAll(GetUserProfile(),GetUserSettings(),GetUserPermissions());

Command Pattern and Idempotency

Command Infrastructure

The library includes built-in support for command pattern with distributed idempotency:

// Basic commandpublicclassCreateOrderCommand:Command<Order>{publicCreateOrderCommand(stringorderId,Orderorder):base(orderId,"CreateOrder"){Value=order;UserId="user123";CorrelationId=Guid.NewGuid().ToString();}}// Command with metadatavarcommand=newCommand("command-id","ProcessPayment"){UserId="user123",SessionId="session456",CorrelationId="correlation789",CausationId="parent-command-id",TraceId=Activity.Current?.TraceId.ToString(),SpanId=Activity.Current?.SpanId.ToString()};

Pagination Commands

Pagination is now a first-class command concept that keeps factories DRY and metadata consistent:

varoptions=newPaginationOptions(defaultPageSize:25,maxPageSize:100);varrequest=PaginationRequest.Create(skip:0,take:0,options);// take defaults to 25// Rich factory surface without duplicate overloadsvarpaginationCommand=PaginationCommand.Create(request,options).WithCorrelationId(Guid.NewGuid().ToString());// Apply to results without manually recalculating metadatavarpage=CollectionResult<Order>.Succeed(orders,paginationCommand.Value!,totalItems:275,options);// Use enum-based command types when desiredenumPaginationCommandType{ListCustomers}vartypedCommand=PaginationCommand.Create(PaginationCommandType.ListCustomers);

PaginationRequest exposes helpers such as Normalize, ClampToTotal, and ToSlice to keep skip/take logic predictable. Configure bounds globally with PaginationOptions to protect APIs from oversized queries.

Idempotent Command Execution

ASP.NET Core Idempotency

// Register idempotency storebuilder.Services.AddSingleton<ICommandIdempotencyStore,InMemoryCommandIdempotencyStore>();// Or use Orleans-based storebuilder.Services.AddSingleton<ICommandIdempotencyStore,OrleansCommandIdempotencyStore>();// Service with idempotent operationspublicclassPaymentService{privatereadonlyICommandIdempotencyStore_idempotencyStore;publicasyncTask<Result<Payment>>ProcessPaymentAsync(ProcessPaymentCommandcommand){// Automatic idempotency - returns cached result if already executedreturnawait_idempotencyStore.ExecuteIdempotentAsync(command.Id,async()=>{// This code runs only once per command IDvarpayment=await_paymentGateway.ChargeAsync(command.Amount);await_repository.SavePaymentAsync(payment);returnResult<Payment>.Succeed(payment);},command.Metadata);}}

Orleans-Based Idempotency

// Automatic idempotency with Orleans grainspublicclassOrderGrain:Grain,IOrderGrain{privatereadonlyICommandIdempotencyStore_idempotencyStore;publicasyncTask<Result<Order>>CreateOrderAsync(CreateOrderCommandcommand){// Uses ICommandIdempotencyGrain internally for distributed coordinationreturnawait_idempotencyStore.ExecuteIdempotentAsync(command.Id,async()=>{// Guaranteed to execute only once across the clustervarorder=newOrder{/* ... */};awaitSaveOrderAsync(order);returnResult<Order>.Succeed(order);});}}

Command Execution Status

publicenumCommandExecutionStatus{NotStarted,// Command hasn't been processedProcessing,// Currently being processedCompleted,// Successfully completedFailed,// Processing failedExpired// Result expired from cache}// Check command statusvarstatus=await_idempotencyStore.GetCommandStatusAsync("command-id");if(status==CommandExecutionStatus.Completed){varresult=await_idempotencyStore.GetCommandResultAsync<Order>("command-id");}

Command Correlation and Tracing Identifiers

Commands implement ICommand and surface correlation, causation, trace, span, user, and session identifiers alongside optional metadata so every hop can attach observability context. The base Command and Command<T> types keep those properties on the root object, and serializers/Orleans surrogates round-trip them without custom plumbing. root object, and serializers/Orleans surrogates round-trip them without custom plumbing.

Identifier lifecycle

  • Static command factories generate monotonic version 7 identifiers via Guid.CreateVersion7() and stamp a UTC timestamp so commands can be sorted chronologically even when sharded.
  • Factory helpers never mutate the correlation or trace identifiers; callers opt in by supplying values through fluent WithCorrelationId, WithTraceId, and similar extension methods that return the same command instance.
  • Metadata mirrors the trace/span identifiers for workload-specific diagnostics without coupling transport-level identifiers to payload annotations.

Field reference

FieldPurposeTypical sourceNotes
CommandIdUnique, monotonic identifier for deduplicationStatic command factoriesRemains stable for retries and storage lookups.
CorrelationIdTies a command to an upstream workflow/requestHTTP X-Correlation-Id, message headersPreserved through
serialization and Orleans surrogates.
CausationIdRecords the predecessor command/eventCurrent command IDSupports causal chains in telemetry.
TraceIdConnects to distributed tracing spansOpenTelemetry/Activity contextThe library stores, but never generate
s, trace identifiers.
SpanIdIdentifies the originating spanOpenTelemetry/Activity contextOften paired with Metadata.TraceId for deep
er traces.
UserId / SessionIdAttach security/session principalsAuthentication middlewareUseful for multi-tenant auditing.

Trace vs. correlation

  • Correlation IDs bundle every command spawned from a single business request. Assign them at ingress and keep the value st able across retries so dashboards can answer “what commands ran because of this call?”.
  • Trace/Span IDs follow distributed tracing semantics. Commands avoid creating new traces and instead persist the ambient A ctivity identifiers through serialization so telemetry back-ends can stitch spans together.
  • Both identifier sets are serialized together, enabling pivots between business-level correlation and technical call graphs wit hout extra configuration.

Generation and propagation guidance

  • Use Command.Create(...) / Command<T>.Create(...) (or the matching From(...) helpers) to get a version 7 identifier and U TC timestamp automatically.
  • Read or generate correlation IDs from HTTP headers or upstream messages and apply them via .WithCorrelationId(...) before d ispatching commands.
  • Capture Activity.TraceId/Activity.SpanId through .WithTraceId(...) and .WithSpanId(...) (and metadata counterparts) wh en bridging to queues, Orleans, or background pipelines.
  • Serialization tests verify the identifiers round-trip, so consumers can rely on receiving the same values they emitted.

Operational considerations

  • Factory unit tests ensure commands created through the helpers carry version 7 identifiers, UTC timestamps, and derived Comma ndType values for traceability.
  • Idempotency regression tests assert that concurrent callers reuse cached results and propagate failures consistently, preservi ng correlation integrity when retry storms occur.

Idempotency Architecture Overview

Scope

The shared idempotency helpers (CommandIdempotencyExtensions), default in-memory store, and test coverage work together to pro tect concurrency, caching, and retry behaviour across hosts.

Strengths

  • Deterministic status transitions.ExecuteIdempotentAsync only invokes the provided delegate after atomically claiming th e command, writes the result, and then flips the status to Completed, so retries either reuse cached output or wait for the in -flight execution to finish.
  • Batch reuse of cached outputs. Batch helpers perform bulk status/result lookups and bypass execution for already completed commands, even when cached results are null or default values.
  • Fine-grained locking in the memory store. Per-command SemaphoreSlim instances eliminate global contention, and reference counting ensures locks are released once no callers use a key.
  • Concurrency regression tests. Dedicated unit tests confirm that concurrent callers share a single execution, failed primar y runs surface consistent exceptions, and the final status ends up in Failed when appropriate.

Risks & considerations

  • Missing-result ambiguity. If a store reports Completed but the result entry expired, the extensions currently return the default value. Stores that can distinguish “missing” from “stored default” should override TryGetCachedResultAsync to trigger a re-execution.
  • Wait semantics rely on polling. Adaptive polling keeps responsiveness reasonable, but distributed stores can swap in push- style notifications if tail latency becomes critical.
  • Status retention policies. The memory store’s cleanup removes status and result after a TTL; other implementations must pr ovide similar hygiene to avoid unbounded growth while keeping enough history for retries.

Recommendations

  1. Document store-specific retention guarantees so callers can tune retry windows.
  2. Consider extending the store contract with a boolean flag (or sentinel wrapper) that differentiates cached default values f rom missing entries.
  3. Monitor lock-pool growth in long-lived applications and log keys that never release to diagnose misbehaving callers before me mory pressure builds up.

Error Handling Patterns

Validation Pattern

publicResult<User>CreateUser(CreateUserDtodto){// Collect all validation errorsvarerrors=newList<(stringfield,stringmessage)>();if(string.IsNullOrEmpty(dto.Email))errors.Add(("email","Email is required"));if(!dto.Email.Contains("@"))errors.Add(("email","Invalid email format"));if(dto.Age<0)errors.Add(("age","Age must be positive"));if(dto.Age<18)errors.Add(("age","Must be 18 or older"));if(errors.Any())returnResult<User>.FailValidation(errors.ToArray());varuser=newUser{/* ... */};returnResult<User>.Succeed(user);}

Repository Pattern with Entity Framework

publicclassUserRepository{privatereadonlyAppDbContext_context;privatereadonlyILogger<UserRepository>_logger;publicasyncTask<Result<User>>GetByIdAsync(intid){try{varuser=await_context.Users.AsNoTracking().FirstOrDefaultAsync(u =>u.Id==id);if(user==null)returnResult<User>.FailNotFound($"User {id} not found");returnResult<User>.Succeed(user);}catch(Exceptionex){_logger.LogError(ex,"Database error getting user {UserId}",id);returnResult<User>.Fail(ex);}}publicasyncTask<CollectionResult<User>>GetPagedAsync(intpage,intpageSize,Expression<Func<User,bool>>?filter=null,Expression<Func<User,object>>?orderBy=null){try{// Build query with IQueryable for efficient SQL generationIQueryable<User>query=_context.Users.AsNoTracking();// Apply filter if providedif(filter!=null)query=query.Where(filter);// Apply orderingquery=orderBy!=null?query.OrderBy(orderBy):query.OrderBy(u =>u.Id);// Get total count - generates COUNT(*) SQL queryvartotalItems=awaitquery.CountAsync();if(totalItems==0)returnCollectionResult<User>.Succeed(Array.Empty<User>(),page,pageSize,0);// Get page of data - generates SQL with OFFSET and FETCHvarusers=awaitquery.Skip((page-1)*pageSize).Take(pageSize).ToArrayAsync();returnCollectionResult<User>.Succeed(users,page,pageSize,totalItems);}catch(Exceptionex){_logger.LogError(ex,"Database error in GetPagedAsync");returnCollectionResult<User>.Fail(ex);}}// Example with complex querypublicasyncTask<CollectionResult<UserDto>>SearchUsersAsync(stringsearchTerm,intpage,intpageSize){try{varquery=_context.Users.AsNoTracking().Where(u =>u.IsActive).Where(u =>EF.Functions.Like(u.Name,$"%{searchTerm}%")||EF.Functions.Like(u.Email,$"%{searchTerm}%"));// Count before projection for efficiencyvartotalItems=awaitquery.CountAsync();// Project to DTO and paginate - single SQL queryvarusers=awaitquery.OrderBy(u =>u.Name).Skip((page-1)*pageSize).Take(pageSize).Select(u =>newUserDto{Id=u.Id,Name=u.Name,Email=u.Email,LastLoginDate=u.LastLoginDate}).ToArrayAsync();returnCollectionResult<UserDto>.Succeed(users,page,pageSize,totalItems);}catch(Exceptionex){_logger.LogError(ex,"Search failed for term: {SearchTerm}",searchTerm);returnCollectionResult<UserDto>.Fail(ex);}}}

Service Layer Pattern

publicclassOrderService{publicasyncTask<Result<Order>>CreateOrderAsync(CreateOrderDtodto){// Validate inputvarvalidationResult=ValidateOrderDto(dto);if(validationResult.IsFailed)returnvalidationResult;// Get uservaruserResult=await_userRepo.GetByIdAsync(dto.UserId);if(userResult.IsFailed)returnResult<Order>.Fail(userResult.Problem);// Check permissionsvaruser=userResult.Value;if(!user.CanCreateOrders)returnResult<Order>.FailForbidden("User cannot create orders");// Create orderreturnawaitResult.Try(async()=>{varorder=newOrder{UserId=user.Id,Items=dto.Items,Total=CalculateTotal(dto.Items)};await_orderRepo.SaveAsync(order);returnorder;});}}

Integration Guides

ASP.NET Core Integration

Installation and Setup

// 1. Install NuGet package// dotnet add package ManagedCode.Communication.AspNetCore// 2. Program.cs configurationvarbuilder=WebApplication.CreateBuilder(args);// Method 1: Simple configuration with auto-detection of environmentbuilder.AddCommunication();// ShowErrorDetails = IsDevelopment// Method 2: Custom configurationbuilder.Services.AddCommunication(options =>{options.ShowErrorDetails=true;// Show detailed error messages in responses});// 3. Add filters to MVC controllers (ORDER MATTERS!)builder.Services.AddControllers(options =>{options.AddCommunicationFilters();// Filters are applied in this order:// 1. CommunicationModelValidationFilter - Catches validation errors first// 2. ResultToActionResultFilter - Converts Result to HTTP response// 3. CommunicationExceptionFilter - Catches any unhandled exceptions last});// 4. Optional: Add filters to SignalR hubsbuilder.Services.AddSignalR(options =>{options.AddCommunicationFilters();});varapp=builder.Build();

Filter Execution Order

The order of filters is important for proper error handling:

OrderFilterPurposeWhen It Runs
1CommunicationModelValidationFilterConverts ModelState errors to Result.FailValidationBefore action execution if model is invalid
2ResultToActionResultFilterMaps Result<T> return values to HTTP responsesAfter action execution
3CommunicationExceptionFilterCatches unhandled exceptions, returns Problem DetailsOn any exception

⚠️Important: The filters must be registered using AddCommunicationFilters() to ensure correct ordering. Manual registration may cause unexpected behavior.

Controller Implementation

[ApiController][Route("api/[controller]")]publicclassUsersController:ControllerBase{privatereadonlyIUserService_userService;[HttpGet("{id}")][ProducesResponseType(typeof(User),200)][ProducesResponseType(typeof(Problem),404)]publicasyncTask<Result<User>>GetUser(intid){returnawait_userService.GetUserAsync(id);}[HttpPost][ProducesResponseType(typeof(User),201)][ProducesResponseType(typeof(Problem),400)]publicasyncTask<Result<User>>CreateUser([FromBody]CreateUserDtodto){returnawait_userService.CreateUserAsync(dto);}[HttpGet][ProducesResponseType(typeof(CollectionResult<User>),200)]publicasyncTask<CollectionResult<User>>GetUsers([FromQuery]intpage=1,[FromQuery]intpageSize=10){returnawait_userService.GetUsersAsync(page,pageSize);}}

Automatic HTTP Response Mapping

The library automatically converts Result types to appropriate HTTP responses:

Result StateHTTP StatusResponse Body
Result.Succeed()204 No ContentEmpty
Result<T>.Succeed(value)200 OKvalue
Result.FailValidation(...)400 Bad RequestProblem Details
Result.FailUnauthorized()401 UnauthorizedProblem Details
Result.FailForbidden()403 ForbiddenProblem Details
Result.FailNotFound()404 Not FoundProblem Details
Result.Fail(...)500 Internal Server ErrorProblem Details

SignalR Integration

publicclassChatHub:Hub{publicasyncTask<Result<MessageDto>>SendMessage(stringuser,stringmessage){if(string.IsNullOrEmpty(message))returnResult<MessageDto>.FailValidation(("message","Message cannot be empty"));varmessageDto=newMessageDto{User=user,Message=message,Timestamp=DateTime.UtcNow};awaitClients.All.SendAsync("ReceiveMessage",user,message);returnResult<MessageDto>.Succeed(messageDto);}publicasyncTask<Result>JoinGroup(stringgroupName){if(string.IsNullOrEmpty(groupName))returnResult.FailValidation(("groupName","Group name is required"));awaitGroups.AddToGroupAsync(Context.ConnectionId,groupName);returnResult.Succeed();}}

Microsoft Orleans Integration

Setup

// Silo configurationvarbuilder=Host.CreateDefaultBuilder(args).UseOrleans(silo =>{silo.UseLocalhostClustering().UseOrleansCommunication();// Required for Result serialization});// Client configuration varclientBuilder=Host.CreateDefaultBuilder(args).UseOrleansClient(client =>{client.UseOrleansCommunication();// Required for Result serialization});

That's it! The UseOrleansCommunication() extension automatically configures:

  • Serialization for all Result types across grain boundaries
  • Proper handling of Problem Details in distributed calls
  • Support for CollectionResult with pagination
  • Exception-to-failed-result conversion for grain methods returning Task<Result>, Task<Result<T>>, Task<CollectionResult<T>>, and matching ValueTask<> forms
  • Structured error logging with the original exception object before a grain exception is converted to a failed Result, so observability backends keep the real stack trace

Grain Implementation

publicinterfaceIUserGrain:IGrainWithStringKey{Task<Result<UserState>>GetStateAsync();Task<Result>UpdateProfileAsync(UpdateProfileDtodto);Task<CollectionResult<Activity>>GetActivitiesAsync(intpage,intpageSize);}publicclassUserGrain:Grain,IUserGrain{privatereadonlyIPersistentState<UserState>_state;publicUserGrain([PersistentState("user")]IPersistentState<UserState>state){_state=state;}publicTask<Result<UserState>>GetStateAsync(){if(!_state.RecordExists)returnTask.FromResult(Result<UserState>.FailNotFound("User not found"));returnTask.FromResult(Result<UserState>.Succeed(_state.State));}publicasyncTask<Result>UpdateProfileAsync(UpdateProfileDtodto){if(!_state.RecordExists)returnResult.FailNotFound("User not found");// Validateif(string.IsNullOrEmpty(dto.DisplayName))returnResult.FailValidation(("displayName","Display name is required"));// Update state_state.State.DisplayName=dto.DisplayName;_state.State.Bio=dto.Bio;_state.State.UpdatedAt=DateTime.UtcNow;await_state.WriteStateAsync();returnResult.Succeed();}publicasyncTask<CollectionResult<Activity>>GetActivitiesAsync(intpage,intpageSize){if(!_state.RecordExists)returnCollectionResult<Activity>.FailNotFound("User not found");// For real data, use a repository with Entity Frameworkvarrepository=GrainFactory.GetGrain<IActivityRepositoryGrain>(0);returnawaitrepository.GetUserActivitiesAsync(this.GetPrimaryKeyString(),page,pageSize);}}

Performance

Best Practices

  1. Use structs: Result and Result<T> are value types (structs) to avoid heap allocation
  2. Avoid boxing: Use generic methods to prevent boxing of value types
  3. Chain operations: Use railway-oriented programming to avoid intermediate variables
  4. Async properly: Use ConfigureAwait(false) in library code
  5. Cache problems: Reuse common Problem instances for frequent errors

Testing

The repository uses xUnit with Shouldly for assertions. Shared matchers such as ShouldBeEquivalentTo and AssertProblem() live in ManagedCode.Communication.Tests/TestHelpers, keeping tests fluent without FluentAssertions.

  • Run the full suite: dotnet test ManagedCode.Communication.Tests/ManagedCode.Communication.Tests.csproj
  • Generate lcov coverage: dotnet test ManagedCode.Communication.Tests/ManagedCode.Communication.Tests.csproj /p:CollectCoverage=true /p:CoverletOutputFormat=lcov

Execution helpers (Result.From, Result<T>.From, task/value-task shims) and the command metadata extensions now have direct tests, pushing the core assembly above 80% line coverage. Mirror those patterns when adding APIs—exercise both success and failure paths and prefer invoking the public fluent surface instead of internal helpers.

Comparison

Comparison with Other Libraries

FeatureManagedCode.CommunicationFluentResultsCSharpFunctionalExtensionsErrorOr
Multiple Errors✅ Yes✅ Yes❌ No✅ Yes
Railway-Oriented✅ Full✅ Full✅ Full⚠️ Limited
HTTP Integration✅ Built-in❌ No⚠️ Extension❌ No
Orleans Support✅ Built-in❌ No❌ No❌ No
SignalR Support✅ Built-in❌ No❌ No❌ No
RFC 7807✅ Full❌ No❌ No❌ No
Pagination✅ Built-in❌ No❌ No❌ No
Command Pattern✅ Built-in❌ No❌ No❌ No
Performance✅ Struct-based❌ Class-based✅ Struct-based✅ Struct-based
Async Support✅ Full✅ Full✅ Full✅ Full

When to Use ManagedCode.Communication

Choose this library when you need:

  • Full-stack integration: ASP.NET Core + SignalR + Orleans
  • Standardized errors: RFC 7807 Problem Details
  • Pagination: Built-in collection results with paging
  • Command pattern: Command infrastructure with idempotency
  • Performance: Struct-based implementation for minimal overhead

Best Practices

DO ✅

// DO: Use Result for operations that can failpublicResult<User>GetUser(intid){varuser=_repository.FindById(id);returnuser!=null?Result<User>.Succeed(user):Result<User>.FailNotFound($"User {id} not found");}// DO: Chain operations using railway-oriented programmingpublicResult<Order>ProcessOrder(OrderDtodto){returnValidateOrder(dto).Then(CreateOrder).Then(CalculateTotals).Then(ApplyDiscounts).Then(SaveOrder);}// DO: Provide specific error informationpublicResultValidateEmail(stringemail){if(string.IsNullOrEmpty(email))returnResult.FailValidation(("email","Email is required"));if(!email.Contains("@"))returnResult.FailValidation(("email","Invalid email format"));returnResult.Succeed();}// DO: Use CollectionResult for paginated datapublicCollectionResult<Product>GetProducts(intpage,intpageSize){varproducts=_repository.GetPaged(page,pageSize);vartotal=_repository.Count();returnCollectionResult<Product>.Succeed(products,page,pageSize,total);}

DON'T ❌

// DON'T: Throw exceptions from Result-returning methodspublicResult<User>GetUser(intid){if(id<=0)thrownewArgumentException("Invalid ID");// ❌ Don't throw// Instead:if(id<=0)returnResult<User>.FailValidation(("id","ID must be positive"));// ✅}// DON'T: Ignore Result valuesvarresult=UpdateUser(user);// ❌ Result ignoredDoSomethingElse();// Instead:varresult=UpdateUser(user);if(result.IsFailed)returnresult;// ✅ Handle the failure// DON'T: Mix Result and exceptionspublicasyncTask<User>GetUserMixed(intid){varresult=awaitGetUserAsync(id);if(result.IsFailed)thrownewException(result.Problem.Detail);// ❌ Mixing patternsreturnresult.Value;}// DON'T: Create generic error messagesreturnResult.Fail("Error");// ❌ Too vague// Instead:returnResult.Fail("User creation failed","Email already exists");// ✅

Examples

Complete Web API Example

// Domain ModelpublicclassProduct{publicintId{get;set;}publicstringName{get;set;}publicdecimalPrice{get;set;}publicintStock{get;set;}}// Service InterfacepublicinterfaceIProductService{Task<Result<Product>>GetByIdAsync(intid);Task<Result<Product>>CreateAsync(CreateProductDtodto);Task<Result>UpdateStockAsync(intid,intquantity);Task<CollectionResult<Product>>SearchAsync(stringquery,intpage,intpageSize);}// Service ImplementationpublicclassProductService:IProductService{privatereadonlyIProductRepository_repository;privatereadonlyILogger<ProductService>_logger;publicasyncTask<Result<Product>>GetByIdAsync(intid){returnawaitResult.Try(async()=>{varproduct=await_repository.FindByIdAsync(id);returnproduct??thrownewKeyNotFoundException($"Product {id} not found");}).CompensateAsync(async error =>{_logger.LogWarning("Product {Id} not found, checking archive",id);vararchived=await_repository.FindInArchiveAsync(id);returnarchived!=null?Result<Product>.Succeed(archived):Result<Product>.FailNotFound($"Product {id} not found");});}publicasyncTask<Result<Product>>CreateAsync(CreateProductDtodto){// ValidationvarvalidationResult=awaitValidateProductDto(dto);if(validationResult.IsFailed)returnResult<Product>.Fail(validationResult.Problem);// Check for duplicatesvarexisting=await_repository.FindByNameAsync(dto.Name);if(existing!=null)returnResult<Product>.Fail("Duplicate product",$"Product with name '{dto.Name}' already exists");// Create productvarproduct=newProduct{Name=dto.Name,Price=dto.Price,Stock=dto.InitialStock};await_repository.AddAsync(product);await_repository.SaveChangesAsync();returnResult<Product>.Succeed(product);}publicasyncTask<Result>UpdateStockAsync(intid,intquantity){returnawaitGetByIdAsync(id).Then(product =>{if(product.Stock+quantity<0)returnResult.Fail("Insufficient stock",$"Cannot reduce stock by {Math.Abs(quantity)}. Current stock: {product.Stock}");product.Stock+=quantity;returnResult.Succeed();}).ThenAsync(async()=>{await_repository.SaveChangesAsync();returnResult.Succeed();});}publicasyncTask<CollectionResult<Product>>SearchAsync(stringquery,intpage,intpageSize){try{var(products,total)=await_repository.SearchAsync(query,page,pageSize);returnCollectionResult<Product>.Succeed(products,page,pageSize,total);}catch(Exceptionex){_logger.LogError(ex,"Search failed for query: {Query}",query);returnCollectionResult<Product>.Fail(ex);}}privateasyncTask<Result>ValidateProductDto(CreateProductDtodto){varerrors=newList<(stringfield,stringmessage)>();if(string.IsNullOrWhiteSpace(dto.Name))errors.Add(("name","Product name is required"));elseif(dto.Name.Length>100)errors.Add(("name","Product name must be 100 characters or less"));if(dto.Price<=0)errors.Add(("price","Price must be greater than zero"));if(dto.InitialStock<0)errors.Add(("initialStock","Initial stock cannot be negative"));// Async validationif(!string.IsNullOrWhiteSpace(dto.Name)){varcategoryExists=await_repository.CategoryExistsAsync(dto.CategoryId);if(!categoryExists)errors.Add(("categoryId","Invalid category"));}returnerrors.Any()?Result.FailValidation(errors.ToArray()):Result.Succeed();}}// Controller[ApiController][Route("api/[controller]")]publicclassProductsController:ControllerBase{privatereadonlyIProductService_productService;[HttpGet("{id}")]publicasyncTask<Result<Product>>Get(intid){returnawait_productService.GetByIdAsync(id);}[HttpPost]publicasyncTask<Result<Product>>Create([FromBody]CreateProductDtodto){returnawait_productService.CreateAsync(dto);}[HttpPatch("{id}/stock")]publicasyncTask<Result>UpdateStock(intid,[FromBody]UpdateStockDtodto){returnawait_productService.UpdateStockAsync(id,dto.Quantity);}[HttpGet("search")]publicasyncTask<CollectionResult<Product>>Search([FromQuery]stringq,[FromQuery]intpage=1,[FromQuery]intpageSize=20){returnawait_productService.SearchAsync(q,page,pageSize);}}

Complex Business Logic Example

publicclassOrderProcessingService{publicasyncTask<Result<Order>>ProcessOrderAsync(ProcessOrderCommandcommand){// Complete order processing pipelinereturnawaitResult// Validate command.From(()=>ValidateCommand(command))// Load user.ThenAsync(async()=>await_userRepository.GetByIdAsync(command.UserId))// Check user permissions.Then(user =>user.CanPlaceOrders?Result<User>.Succeed(user):Result<User>.FailForbidden("User cannot place orders"))// Verify user credit.ThenAsync(async user =>await_creditService.CheckCreditAsync(user.Id)).Then(creditResult =>creditResult.AvailableCredit>=command.TotalAmount?Result.Succeed():Result.Fail("Insufficient credit"))// Check inventory.ThenAsync(async()=>awaitCheckInventoryAsync(command.Items))// Reserve inventory.ThenAsync(async()=>awaitReserveInventoryAsync(command.Items))// Create order.ThenAsync(async()=>awaitCreateOrderAsync(command))// Process payment.ThenAsync(async order =>awaitProcessPaymentAsync(order,command.PaymentMethod))// Send confirmation.ThenAsync(async order =>awaitSendOrderConfirmationAsync(order))// Handle any failures.CompensateAsync(async problem =>{_logger.LogError("Order processing failed: {Problem}",problem.Detail);// Rollback inventory reservationawaitReleaseInventoryAsync(command.Items);// Notify userawait_notificationService.NotifyOrderFailedAsync(command.UserId,problem.Detail);returnResult<Order>.Fail(problem);});}privateasyncTask<Result>CheckInventoryAsync(List<OrderItem>items){varunavailable=newList<string>();foreach(variteminitems){varstock=await_inventoryService.GetStockAsync(item.ProductId);if(stock<item.Quantity){unavailable.Add($"{item.ProductName}: requested {item.Quantity}, available {stock}");}}returnunavailable.Any()?Result.Fail("Insufficient inventory",string.Join("; ",unavailable)):Result.Succeed();}}

Migration Guide

Migrating from Exceptions

Before (Exception-based)

publicUserGetUser(intid){if(id<=0)thrownewArgumentException("Invalid ID");varuser=_repository.FindById(id);if(user==null)thrownewNotFoundException($"User {id} not found");if(!user.IsActive)thrownewInvalidOperationException("User is not active");returnuser;}// Usagetry{varuser=GetUser(id);// Process user}catch(ArgumentExceptionex){// Handle validation error}catch(NotFoundExceptionex){// Handle not found}catch(Exceptionex){// Handle other errors}

After (Result-based)

publicResult<User>GetUser(intid){if(id<=0)returnResult<User>.FailValidation(("id","ID must be positive"));varuser=_repository.FindById(id);if(user==null)returnResult<User>.FailNotFound($"User {id} not found");if(!user.IsActive)returnResult<User>.Fail("User inactive","User account is not active");returnResult<User>.Succeed(user);}// Usagevarresult=GetUser(id);result.Match(onSuccess: user =>{/* Process user */},onFailure: problem =>{if(result.IsInvalid){// Handle validation error}elseif(problem.StatusCode==404){// Handle not found}else{// Handle other errors}});

Gradual Migration Strategy

  1. Start with new code: Implement Result pattern in new features
  2. Wrap existing methods: Use Result.Try() to wrap exception-throwing code
  3. Update interfaces: Change return types from T to Result<T>
  4. Convert controllers: Update API endpoints to return Result types
  5. Remove try-catch blocks: Replace with Result pattern handling
// Step 1: Wrap existing codepublicResult<User>GetUserSafe(intid){returnResult.Try(()=>GetUserUnsafe(id));}// Step 2: Gradually refactor internalspublicResult<User>GetUserRefactored(intid){// Refactored implementation without exceptions}// Step 3: Update consumerspublicasyncTask<IActionResult>GetUser(intid){varresult=await_service.GetUserRefactored(id);returnresult.Match(onSuccess: user =>Ok(user),onFailure: problem =>Problem(problem));}

Contributing

Contributions are welcome! Fork the repository and submit a pull request.

Development Setup

# Clone the repository
git clone https://github.com/managed-code-hub/Communication.git
# Build the solution
dotnet build
# Run tests
dotnet test# Run benchmarks
dotnet run -c Release --project benchmarks/ManagedCode.Communication.Benchmarks

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support

Acknowledgments

  • Inspired by F# and Rust Result types
  • Railway-oriented programming concepts
  • RFC 7807 Problem Details for HTTP APIs
  • Built for seamless integration with Microsoft Orleans
  • Optimized for ASP.NET Core applications

About

Result pattern for .NET that replaces exceptions with type-safe return values. Features railway-oriented programming, ASP.NET Core integration, RFC 7807 Problem Details, and built-in pagination. Designed for production systems requiring explicit error handling without the overhead of throwing exceptions.

Topics

Resources

Stars

95 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages