Modular .NET NuGet ecosystem that bridges OOP and Functional Programming in C#.
| Package | Description | Docs | NuGet |
|---|---|---|---|
CSharpEssentials |
Meta-package: Results, Errors, Maybe, Any, These, Rules, Core, Enums, Entity, Clone, Json, Http, Time | Docs | |
CSharpEssentials.Results |
Result<T>: railway-oriented error handling without exceptions |
Docs | |
CSharpEssentials.Errors |
Structured Error values with type, code, description, metadata |
Docs | |
CSharpEssentials.Maybe |
Maybe<T>: explicit optionals, no null reference exceptions |
Docs | |
CSharpEssentials.Any |
Any<T1,T2,...>: type-safe discriminated unions |
Docs | |
CSharpEssentials.These |
These<TError,TValue>: 3-state union with Left, Right, Both (error + value simultaneously) |
Docs | |
CSharpEssentials.Core |
String/GUID/collection utilities | Docs | |
CSharpEssentials.Enums |
[StringEnum] source generator, AOT-safe enum↔string |
Docs | |
CSharpEssentials.Rules |
Composable rule engine with .And()/.Or()/.Linear()/.Next() |
Docs | |
CSharpEssentials.Mediator |
CQRS pipeline behaviors for the Mediator source generator: validation, logging, exception handling, caching, locking, transactions | Docs | |
CSharpEssentials.Entity |
EntityBase<TId>, soft deletion, domain events |
Docs | |
CSharpEssentials.EntityFrameworkCore |
EF Core interceptors (audit, events, slow queries), offset and keyset pagination, enum storage | Docs | |
CSharpEssentials.AspNetCore |
GlobalExceptionHandler, ResultEndpointFilter, enum binding, API versioning |
Docs | |
CSharpEssentials.AspNetCore.OpenApi |
Enum convention schemas for Microsoft.AspNetCore.OpenApi (net10.0+, Microsoft.OpenApi 2.x); one OpenAPI package per host |
Docs | |
CSharpEssentials.AspNetCore.Swashbuckle |
Swagger versioning, schema ids, security schemes and enum convention schemas for Swashbuckle 10 (net8.0+, Microsoft.OpenApi 2.x); one OpenAPI package per host | Docs | |
CSharpEssentials.Endpoints |
Source-generated Minimal API endpoint discovery (IEndpoint, IEndpointGroup), AOT-safe, analyzers CSE1001 to CSE1011 |
Docs | |
CSharpEssentials.DependencyInjection |
Attribute-based, source-generated service registration and decoration ([RegisterScoped], [Decorates]), analyzers CSE2001 to CSE2009 |
Docs | |
CSharpEssentials.Http |
HttpClient extensions returning Result<T> |
Docs | |
CSharpEssentials.Idempotency |
Transport-independent idempotency: record store with reservation tokens, scoped record keys, scope resolvers, deterministic keys (UUIDv5) | Docs | |
CSharpEssentials.Idempotency.Testing |
xUnit contract tests for custom IIdempotencyRecordStore implementations |
Docs | |
CSharpEssentials.Resilience |
Transient fault handling: Retry, Timeout, Circuit Breaker, Fallback | Docs | |
CSharpEssentials.Json |
Pre-configured System.Text.Json options and converters |
Docs | |
CSharpEssentials.RequestResponseLogging |
Request/response body logging middleware | Docs | |
CSharpEssentials.GcpSecretManager |
GCP Secret Manager → IConfiguration provider |
Docs | |
CSharpEssentials.Time |
Testable IDateTimeProvider wrapping TimeProvider |
Docs | |
CSharpEssentials.Validation |
High-performance model-first validation with Result<T> integration |
Docs | |
CSharpEssentials.Clone |
ICloneable<T>: typed deep-copy for entity collections |
Docs |
API Reference: guide to every package, method, and pattern in the ecosystem. It covers 24 of the 27 packages with method tables, philosophy, code examples, and cross-cutting FP patterns; CSharpEssentials.AspNetCore.OpenApi, CSharpEssentials.AspNetCore.Swashbuckle and CSharpEssentials.Idempotency.Testing are documented in their own Readme files.
# Core functional modules
dotnet add package CSharpEssentials
# Individual packages
dotnet add package CSharpEssentials.Rules
dotnet add package CSharpEssentials.MediatorStop throwing exceptions for expected failures. Chain operations that short-circuit on the first error.
// ❌ Traditional: exceptions as control flow
public User GetUserOrThrow(Guid id)
{
var user = _db.Find(id);
if (user == null) throw new NotFoundException("User not found");
return user;
}
// ✅ Result<T>: explicit, composable error handling
public Result<User> GetUser(Guid id)
{
var user = _db.Find(id);
if (user is null) return Error.NotFound("User.NotFound", "User not found");
return user;
}
// Chain operations — stops at first failure, no try/catch needed
Result<UserDto> result = GetUser(id)
.Bind(user => ValidateAge(user))
.Bind(user => CheckPermissions(user))
.Map(user => new UserDto(user));Model the absence of a value without null references.
// ❌ Null checks everywhere, easy to miss
string display = user?.Profile?.DisplayName?.Trim()?.ToUpper() ?? "ANONYMOUS";
// ✅ Maybe<T>: pipeline that handles absence at every step
string displayName = Maybe.From(user)
.Bind(u => Maybe.From(u.Profile))
.Bind(p => Maybe.From(p.DisplayName))
.Map(n => n.Trim().ToUpper())
.GetValueOrDefault("ANONYMOUS");A value that is exactly one of several types, exhaustively matched at compile time.
Any<Circle, Rectangle, Triangle> shape = new Circle(radius: 5);
AnyActionResult<double> area = shape.Match(
circle => Math.PI * circle.Radius * circle.Radius,
rectangle => rectangle.Width * rectangle.Height,
triangle => 0.5 * triangle.Base * triangle.Height
// compiler error if any case is missing
);Build complex validation logic from simple, reusable rules that return Result.
Func<User, Result> minAge = u => u.Age >= 18 ? Result.Success() : Error.Validation("Age.MinAge", "Must be 18+");
Func<User, Result> validEmail = u => u.Email.Contains('@') ? Result.Success() : Error.Validation("Email.Invalid", "Invalid email");
Func<User, Result> verified = u => u.IsVerified ? Result.Success() : Error.Validation("Account.NotVerified", "Account not verified");
// AND — all rules must pass; stops at the first failure and returns its errors
Result result = RuleEngine.Evaluate(new[] { minAge, validEmail, verified }.And(), user);
// OR — at least one rule must pass
Result adminOrMod = RuleEngine.Evaluate(new[] { isAdmin, isModerator }.Or(), user);Map Result<T> to correct HTTP status codes without writing any error-mapping boilerplate.
// Program.cs
builder.Services.AddEnhancedProblemDetails(); // registers ProblemDetails (see "Migrating from 3.x to 4.0")
// Controller — business logic returns Result<T>, HTTP mapping is automatic
public class ProductsController(IProductService productService) : ControllerBase
{
[HttpGet("{id:guid}")]
public IActionResult GetProduct(Guid id)
=> productService.GetProduct(id).Match(
onSuccess: product => Ok(product),
onError: errors => errors.ToActionResult() // 404 / 400 / 409 based on ErrorType
);
}
// Service — chain multiple steps, first failure short-circuits
public Result<Order> PlaceOrder(Guid productId, int quantity)
=> _productService.GetProduct(productId)
.Then(p => ValidateStock(p, quantity))
.Then(p => ReserveStock(p, quantity))
.Then(p => CreateOrder(p, quantity));Each endpoint is a type with a static Map method. A source generator collects them into one registry per assembly, so mapping uses no reflection and works with Native AOT.
public sealed class ProductsGroup : IEndpointGroup
{
public static string Prefix => "products";
public static void Configure(RouteGroupBuilder group) => group.WithTags("Products");
}
[EndpointGroup<ProductsGroup>]
public sealed class GetProduct : IEndpoint
{
public static void Map(IEndpointRouteBuilder app) =>
app.MapGet("/{id:guid}", (Guid id, IProductService service) =>
service.GetProduct(id).Match(product => Results.Ok(product), errors => errors.ToProblemResult()));
}
// Program.cs
app.MapAllEndpoints(); // every generated registry of the app and its referenced assembliesMark a class with its lifetime. A source generator writes the registrations, and decorators are applied in order.
[RegisterScoped] // registered as IOrderService, by the I{TypeName} rule
public sealed class OrderService : IOrderService { }
[Decorates(typeof(IOrderService), Order = 1)]
public sealed class LoggingOrderService(IOrderService inner) : IOrderService { }
// Program.cs
builder.Services.AddAllServices(); // every generated registry of the app and its referenced assembliesMark an enum with [StringEnum] and every layer reads and writes it the same way, driven by one EnumConventions instance (CSharpEssentials.Enums). The wire name is snake_case by default (PendingApproval → pending_approval); change it per enum ([StringEnum(Naming = ...)]), per member ([JsonStringEnumMemberName]) or per project (the CSharpEssentialsEnumNaming MSBuild property). Enums without [StringEnum] keep the framework defaults.
[StringEnum]
public enum OrderStatus
{
Pending,
[EnumAlias("Approval")] PendingApproval,
[EnumFallback] Unknown = 99, // what tolerant readers map unknown values to
}| Layer | Package | Setup | Reads | Writes |
|---|---|---|---|---|
| JSON (System.Text.Json) | CSharpEssentials.Json |
options.AddEnumConventions(conventions) |
wire name, member name in any casing, alias, defined number; undefined numbers rejected | "pending_approval", or 1 with WriteAs = Number |
| Route, query, header, form | CSharpEssentials.AspNetCore |
AddEnumConventions() + UseEnumBinding() |
same rules as a JSON body, trimmed; invalid → 400 problem listing the allowed values | |
| Responses | CSharpEssentials.AspNetCore |
WithEnumWireFormat(...) per group, [EnumWireFormat] per controller |
string by default, number per group | |
| OpenAPI | CSharpEssentials.AspNetCore.OpenApi or .Swashbuckle (one per host) |
o.AddEnumConventions() |
string or integer schema, value table, x-enum-* |
|
| EF Core | CSharpEssentials.EntityFrameworkCore |
ConfigureEnumConventions(conventions) |
tolerant: any known spelling, unknown → fallback member | text column + ck_{table}_{column}_enum; [Flags] as an integer bitmask |
| Outgoing HTTP | CSharpEssentials.Http |
WithEnumConventions(conventions) on HttpRequestBuilder |
tolerant | wire names in route and query |
| Validation | CSharpEssentials.Validation |
IsDefinedEnum(), IsOneOf(...), HasOnlyDefinedFlags() |
CSharpEssentials.Enums carries the source generator, the analyzers and the code fixes. Any CSharpEssentials package brings the generator and analyzers to your project: every package that depends on Enums, directly or through another CSharpEssentials package, passes them on. A direct CSharpEssentials.Enums reference is not required; add it only when the project uses no other CSharpEssentials package that depends on it (.Core, .Clone, .Time, .DependencyInjection, .Endpoints and .RequestResponseLogging do not depend on it).
| Scenario | Packages |
|---|---|
| ASP.NET Core API | CSharpEssentials.AspNetCore |
| + OpenAPI | CSharpEssentials.AspNetCore.OpenApi (net10.0) or CSharpEssentials.AspNetCore.Swashbuckle, never both |
| + EF Core | CSharpEssentials.EntityFrameworkCore |
| Worker or console with EF Core | CSharpEssentials.EntityFrameworkCore |
| JSON only (message bus, client) | CSharpEssentials.Json |
| Outgoing HTTP | CSharpEssentials.Http |
Generated helpers and EnumMetadata only |
CSharpEssentials.Enums |
One-time setup per host:
// Minimal API: JSON options, binding, body errors and output in one call
builder.Services.AddEnumConventions();
app.UseEnumBinding(); // after routing, UseAuthentication and UseAuthorization
// Body errors as the same 400 problem: Minimal APIs throw a body error only with ThrowOnBadRequest,
// and GlobalExceptionHandler maps it. Without these lines the framework writes its own 400 without a body.
// MVC reports body errors through model state instead.
builder.Services.Configure<RouteHandlerOptions>(o => o.ThrowOnBadRequest = true);
builder.Services.AddEnhancedProblemDetails();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
app.UseEnhancedProblemDetails();
app.MapGet("/orders", (OrderStatus? status) => Results.Ok(status)); // ?status=pending_approval, approval or 1
// MVC: the same two calls; [FromQuery], [FromRoute], [FromHeader], [FromForm] and DTO properties bind
builder.Services.AddControllers();
builder.Services.AddEnumConventions();
app.UseEnumBinding();
// OpenAPI: one document per version, v1 keeps numbers for old clients
builder.Services.AddOpenApi("v1", o => o.AddEnumConventions()); // or AddSwaggerGen(o => o.AddEnumConventions())
builder.Services.AddOpenApi("v2", o => o.AddEnumConventions());
app.MapGroup("/api/v1").WithGroupName("v1").WithEnumWireFormat(EnumWireFormat.Number);
app.MapGroup("/api/v2").WithGroupName("v2");
// EF Core only (worker, console): no ASP.NET Core package needed
protected override void ConfigureConventions(ModelConfigurationBuilder builder) =>
builder.ConfigureEnumConventions(EnumConventions.Default);There is no message bus package. A bus that serializes with System.Text.Json takes the JSON conventions in tolerant Data mode, for example MassTransit: cfg.ConfigureJsonSerializerOptions(o => o.AddEnumConventions(conventions, EnumReadMode.Data, writeAs: EnumWireFormat.Number)). Upgrade consumers first, then switch producers to strings.
[Flags] enums are JSON arrays on the wire (["read","write"]), repeated or comma-separated query values, and an integer bitmask in the database. Give every member an explicit value and a None = 0 member; the analyzers warn otherwise (CSE0005, CSE0006, and CSE0007 for a member that is neither a single bit nor a combination), and [EnumFallback] on flags is an error (CSE0008). Use them for a fixed, small set of independent on/off capabilities (permissions, feature toggles, days of week). Use List<TEnum> for a user-chosen multi-select, a set that grows often or past 64 members, or when a member needs data attached; use a single enum for one value at a time (statuses, types).
The v2 document of the end-to-end sample describes the enum like this (the v1 document has "type": "integer" and "enum": [0, 1, 2, 99]):
"OrderStatus": {
"enum": ["pending", "pending_approval", "shipped", "unknown"],
"type": "string",
"description": "Order lifecycle state.\n\n| value | number | description |\n|---|---|---|\n| `pending` | 0 | Created, waiting for payment. |\n| `pending_approval` | 1 | Paid, waiting for manual approval. |\n| `shipped` | 2 | Shipped to the customer. |\n| `unknown` | 99 | Response only: a value this API version does not know. |",
"x-enum-varnames": ["Pending", "PendingApproval", "Shipped", "Unknown"],
"x-enum-descriptions": ["Created, waiting for payment.", "Paid, waiting for manual approval.", "Shipped to the customer.", "Response only: a value this API version does not know."],
"x-enum-numeric-values": [0, 1, 2, 99]
}Details: CSharpEssentials.Enums Readme (generated API, diagnostics, renaming a member, AOT), enum conventions design (normative matrix, flags guidance, rollout) and ADR-007.
- CHANGELOG.md lists the changes in every release.
- Coming from 6.x with the
Idempotency-Keymiddleware or a custom idempotency store? Read Migrating idempotency from 6.x to 7.0. 7.0 moves the store intoCSharpEssentials.Idempotencyand replacesKeyScopewithScopeResolver. - From 6.4.0, the task-returning members of
ResultandMaybehaveAsyncnames, and the unsuffixed names are obsolete. Read Async naming and the async overload matrix. - Coming from 5.x? Read Migrating from 5.x to 6.0. 6.0 moves the Swashbuckle support to Swashbuckle 10 and Microsoft.OpenApi 2.x on every target, makes the single-column cursor
PaginateAsyncobsolete in favor ofKeysetPaginateAsyncand caps the offset page size at 100 by default. - From 6.1.0, Swashbuckle documents describe optional route parameters (
{id?}) as one path per form with required parameters. Read Optional route parameters for the new output and how to opt back. From 6.2.0,CSharpEssentials.AspNetCore.OpenApioffers the same output as an opt-in (o.AddOptionalRouteParameters()). - Coming from 4.x? Read Migrating from 4.x to 5.0. 5.0 applies one set of enum conventions to JSON, binding, OpenAPI and EF Core, and moves the Swashbuckle support into its own package.
- Coming from 3.x? Read Migrating from 3.x to 4.0. 4.0 changed ProblemDetails defaults, enum naming and some package dependencies.
Install skills for Claude Code, Cursor, Codex, and 50+ other AI agents:
npx skills add senrecep/CSharpEssentialsEach package has a dedicated skill with accurate API examples, correct namespaces, and common pitfalls.