Build resilient .NET applications with Railway Oriented Programming.
Encina is a comprehensive toolkit for building robust .NET applications. Built on LanguageExt, it provides explicit error handling through Either<EncinaError, T>, CQRS patterns, messaging infrastructure, and composable pipeline behaviors.
- Railway Oriented Programming: All operations return
Either<EncinaError, T>- no exceptions for business logic - CQRS Support: Explicit
ICommand<T>andIQuery<T>contracts with dedicated handlers - Pipeline Behaviors: Composable middleware for validation, caching, transactions, and more
- Audit Trail: Automatic tracking of who created/modified entities and when (see docs)
- Soft Delete: Automatic delete-to-update conversion with global query filters (see docs)
- Temporal Tables: SQL Server point-in-time queries and entity history (see docs)
- Streaming:
IAsyncEnumerablesupport viaIStreamRequest<TItem> - Observability: Built-in OpenTelemetry integration with ActivitySource and Metrics
- Pay-for-What-You-Use: All features are opt-in via satellite packages
flowchart TB
subgraph Client
A[Application Code]
end
subgraph Encina["Encina Core"]
B[IEncina]
C[Pipeline Behaviors]
D[Request Handler]
end
subgraph Satellites["Satellite Packages"]
E[Validation]
F[Caching]
G[Transactions]
H[Observability]
end
A -->|Send/Publish| B
B --> C
C --> E & F & G & H
C --> D
D -->|Either of Error,T| B
B -->|Either of Error,T| A
dotnet add package Encinausing Encina;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEncina(typeof(Program).Assembly);public sealed record CreateOrder(Guid CustomerId, List<OrderItem> Items) : ICommand<OrderId>;
public sealed class CreateOrderHandler : ICommandHandler<CreateOrder, OrderId>
{
public async Task<OrderId> Handle(CreateOrder command, CancellationToken ct)
{
var order = Order.Create(command.CustomerId, command.Items);
await _repository.SaveAsync(order, ct);
return order.Id;
}
}var result = await encina.Send(new CreateOrder(customerId, items), ct);
result.Match(
Left: error => logger.LogError("Order failed: {Code}", error.Code),
Right: orderId => logger.LogInformation("Order created: {Id}", orderId));sequenceDiagram
participant App as Application
participant Enc as IEncina
participant Pipe as Pipeline
participant Handler
App->>Enc: Send(command)
Enc->>Pipe: Execute behaviors
loop Each Behavior
Pipe->>Pipe: Validation, Caching, etc.
end
Pipe->>Handler: Handle(command, ct)
Handler-->>Pipe: TResponse
Pipe-->>Enc: Either[EncinaError, T]
Enc-->>App: Either[EncinaError, T]
| Category | Packages | Description |
|---|---|---|
| Core | Encina |
ROP, CQRS, pipeline behaviors |
| Validation | FluentValidation, DataAnnotations, MiniValidator, GuardClauses |
Request validation with ROP integration |
| Web | AspNetCore, SignalR |
Middleware, authorization, real-time notifications |
| Database | EntityFrameworkCore, MongoDB, Dapper.{5}, ADO.{5} |
Persistence with messaging patterns |
| Messaging | Messaging, RabbitMQ, Kafka, AzureServiceBus, AmazonSQS, NATS, MQTT, Redis.PubSub, InMemory, gRPC, GraphQL |
Message transports |
| Caching | Caching, Caching.Memory, Caching.Hybrid, Caching.Redis, Caching.Valkey, Caching.KeyDB, Caching.Dragonfly, Caching.Garnet |
Multi-tier caching |
| Scheduling | Hangfire, Quartz |
Job scheduling adapters |
| Resilience | Extensions.Resilience, Polly, Refit |
Retry, circuit breaker, HTTP clients |
| Event Sourcing | Marten |
Event store with projections |
| Observability | OpenTelemetry |
Distributed tracing and metrics |
Note: 3,800+ tests across all packages
Encina provides three validation approaches, all using the centralized Orchestrator pattern:
flowchart LR
subgraph Request Pipeline
A[Request] --> B[ValidationPipelineBehavior]
end
subgraph Validation
B --> C[ValidationOrchestrator]
C --> D[IValidationProvider]
end
subgraph Providers
D --> E[FluentValidation]
D --> F[DataAnnotations]
D --> G[MiniValidator]
end
E & F & G --> H{Valid?}
H -->|Yes| I[Handler]
H -->|No| J[EncinaError]
// Registration
builder.Services.AddEncinaFluentValidation(typeof(Program).Assembly);
// Validator
public class CreateOrderValidator : AbstractValidator<CreateOrder>
{
public CreateOrderValidator()
{
RuleFor(x => x.CustomerId).NotEmpty();
RuleFor(x => x.Items).NotEmpty();
}
}// Registration
builder.Services.AddDataAnnotationsValidation();
// Request with attributes
public sealed record CreateOrder(
[Required] Guid CustomerId,
[Required, MinLength(1)] List<OrderItem> Items) : ICommand<OrderId>;// Registration
builder.Services.AddMiniValidation();
// Uses DataAnnotations attributes - ultra-lightweight (~20KB)Encina provides enterprise messaging patterns for distributed systems:
flowchart TB
subgraph Outbox Pattern
A1[Command Handler] --> B1[Save Entity]
B1 --> C1[Save OutboxMessage]
C1 --> D1[Commit Transaction]
D1 --> E1[Background Processor]
E1 --> F1[Publish to Transport]
end
subgraph Inbox Pattern
A2[Message Received] --> B2[Check InboxMessage]
B2 -->|New| C2[Process Message]
B2 -->|Duplicate| D2[Return Cached Response]
C2 --> E2[Save to Inbox]
end
builder.Services.AddEncinaEntityFrameworkCore<AppDbContext>(config =>
{
config.UseTransactions = true; // Automatic transaction management
config.UseOutbox = true; // Reliable event publishing
config.UseInbox = true; // Idempotent message processing
config.UseSagas = true; // Distributed transactions
config.UseScheduling = true; // Delayed/recurring execution
});Encina supports immutable domain entities (C# records) with EF Core while preserving domain events:
// Define an immutable aggregate root
public record Order : AggregateRoot<OrderId>
{
public required string CustomerName { get; init; }
public required OrderStatus Status { get; init; }
public Order Ship()
{
AddDomainEvent(new OrderShippedEvent(Id));
return this with { Status = OrderStatus.Shipped };
}
}
// Update with event preservation (DbContext)
var order = await context.Orders.FindAsync(orderId);
var shippedOrder = order!.Ship().WithPreservedEvents(order);
var result = context.UpdateImmutable(shippedOrder);
await context.SaveChangesAsync(); // Events dispatched automatically
// Or with IUnitOfWork pattern
var orderResult = await unitOfWork.Repository<Order, Guid>().GetByIdAsync(orderId, ct);
var updateResult = orderResult.Bind(order =>
{
var shipped = order.Ship().WithPreservedEvents(order);
return unitOfWork.UpdateImmutable(shipped);
});
await unitOfWork.SaveChangesAsync(ct);See Immutable Domain Models for full documentation.
// SQL Server with Dapper
builder.Services.AddEncinaDapper(config =>
{
config.UseOutbox = true;
config.UseInbox = true;
}, connectionString);// Configure caching
builder.Services.AddEncinaCaching(options =>
{
options.EnableQueryCaching = true;
options.DefaultDuration = TimeSpan.FromMinutes(10);
});
// Add a cache provider
builder.Services.AddEncinaRedisCache("localhost:6379");
// Or: AddEncinaMemoryCache(), AddEncinaHybridCache(), etc.
// Mark queries as cacheable
[Cache(Duration = 300)]
public sealed record GetProductById(Guid Id) : IQuery<Product>;builder.Services.AddEncinaStandardResilience(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.CircuitBreaker.FailureRatio = 0.5;
options.Timeout.Timeout = TimeSpan.FromSeconds(30);
});// Define a stream request
public sealed record StreamProducts(string Category) : IStreamRequest<Product>;
public sealed class StreamProductsHandler : IStreamRequestHandler<StreamProducts, Product>
{
public async IAsyncEnumerable<Product> Handle(
StreamProducts request,
[EnumeratorCancellation] CancellationToken ct)
{
await foreach (var product in _repository.StreamByCategory(request.Category, ct))
{
yield return product;
}
}
}
// Consume the stream
await foreach (var result in encina.Stream(new StreamProducts("Electronics"), ct))
{
result.Match(
Left: error => logger.LogError("Stream error: {Error}", error.Message),
Right: product => Console.WriteLine(product.Name));
}builder.Services.AddEncinaSignalR();
// Declarative broadcasting
[BroadcastToSignalR("OrderUpdated")]
public sealed record OrderStatusChanged(Guid OrderId, string Status) : INotification;builder.Services.AddOpenTelemetry()
.WithTracing(b => b.AddSource("Encina"))
.WithMetrics(b => b.AddMeter("Encina"));Encina provides automatic health check registration for all providers. When you configure a provider, health checks are registered automatically.
// Health checks registered automatically
builder.Services.AddEncinaDapper<PostgreSqlConnection>(config =>
{
config.UseOutbox = true;
config.ProviderHealthCheck.Enabled = true; // Default
});
// Configure ASP.NET Core health endpoints
builder.Services.AddHealthChecks();
app.MapHealthChecks("/health");
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = check => check.Tags.Contains("ready")
});| Category | Providers | Default Tags |
|---|---|---|
| Databases | PostgreSQL, MySQL, SQL Server, SQLite, Oracle, MongoDB, Marten | database, ready |
| Message Brokers | RabbitMQ, Kafka, NATS, MQTT, Azure Service Bus, Amazon SQS | messaging, ready |
| Caching | Redis (and compatible: Valkey, KeyDB, Dragonfly, Garnet) | caching, ready |
| Scheduling | Hangfire, Quartz | scheduling, ready |
| Other | SignalR, gRPC, EF Core | messaging/database, ready |
See Encina.Messaging README for detailed configuration options.
public sealed class LoggingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public async ValueTask<Either<EncinaError, TResponse>> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
_logger.LogInformation("Handling {Request}", typeof(TRequest).Name);
var result = await next();
result.Match(
Left: error => _logger.LogWarning("Failed: {Error}", error.Code),
Right: _ => _logger.LogInformation("Completed successfully"));
return result;
}
}src/
├── Encina/ # Core library
├── Encina.Messaging/ # Messaging abstractions
├── Encina.AspNetCore/ # Web integration
├── Encina.EntityFrameworkCore/# EF Core provider
├── Encina.Dapper.*/ # Dapper providers (5 databases)
├── Encina.ADO.*/ # ADO.NET providers (5 databases)
├── Encina.Caching.*/ # Caching providers (8 packages)
├── Encina.FluentValidation/ # FluentValidation integration
└── ... # 39 packages total
tests/
├── Encina.UnitTests/ # Consolidated unit tests
├── Encina.IntegrationTests/ # Integration tests
├── Encina.PropertyTests/ # Property-based tests
├── Encina.ContractTests/ # API contract tests
├── Encina.GuardTests/ # Guard clause tests
├── Encina.LoadTests/ # Load testing harness
├── Encina.NBomber/ # NBomber scenarios
└── Encina.BenchmarkTests/ # BenchmarkDotNet benchmarks
# Run all tests
dotnet test Encina.slnx --configuration Release
# Run with coverage
dotnet test --collect:"XPlat Code Coverage"
# Run mutation testing
dotnet run --file scripts/run-stryker.cs- ROADMAP.md - Development phases and planned features
- CHANGELOG.md - Version history
- docs/releases/ - Version-based release documentation
- docs/messaging/ - Saga patterns and transport guides
Encina is in active development toward 1.0. See ROADMAP.md for details.
| Phase | Status | Description |
|---|---|---|
| Phase 1 | ✅ Complete | Stability - All tests passing |
| Phase 2 | In Progress | Functionality - 364 issues across 10 milestones |
| Phase 3 | Pending | Testing & Quality - Coverage targets (85%+) |
| Phase 4 | Pending | Code Quality - SonarCloud compliance |
| Phase 5 | Pending | Documentation - User guides and examples |
| Phase 6 | Pending | Release - NuGet publishing, branding |
| Version | Focus | Issues |
|---|---|---|
| v0.10.0 | DDD Foundations | 31 |
| v0.11.0 | Testing Infrastructure | 25 |
| v0.12.0 | Database & Repository | 22 |
| v0.13.0 | Security & Compliance | 25 |
| v0.14.0 | Cloud-Native & Aspire | 23 |
| v0.15.0 | Messaging & EIP | 71 |
| v0.16.0 | Multi-Tenancy & Modular | 21 |
| v0.17.0 | AI/LLM Patterns | 16 |
| v0.18.0 | Developer Experience | 43 |
| v0.19.0 | Observability & Resilience | 87 |
See CONTRIBUTING.md for guidelines.
This project is licensed under the MIT License.
Maintained by: @dlrivada