i26.AspNetCore 0.5.0

dotnet add package i26.AspNetCore --version 0.5.0
                    
NuGet\Install-Package i26.AspNetCore -Version 0.5.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="i26.AspNetCore" Version="0.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="i26.AspNetCore" Version="0.5.0" />
                    
Directory.Packages.props
<PackageReference Include="i26.AspNetCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add i26.AspNetCore --version 0.5.0
                    
#r "nuget: i26.AspNetCore, 0.5.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package i26.AspNetCore@0.5.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=i26.AspNetCore&version=0.5.0
                    
Install as a Cake Addin
#tool nuget:?package=i26.AspNetCore&version=0.5.0
                    
Install as a Cake Tool

i26.AspNetCore

The boundary: where an i26 Result becomes an HTTP response, and where an endpoint says what it can answer.

dotnet add package i26.AspNetCore

ASP.NET Core

Problem responses

ProblemResults.Problem turns a failed result into application/problem+json, per RFC 9457:

return result.Match(Results.Ok, ProblemResults.Problem);          // Result<T>
return result.Match(() => Results.Ok(), ProblemResults.Problem);  // Result

Both sides of the fold have to answer the same type, and both of these answer IResult. The no-value form needs the lambda because Results.Ok has an optional parameter, and a method group with one does not convert to Func<IResult>.

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
  "title": "course.notFound",
  "status": 404,
  "detail": "Course not found",
  "code": "course.notFound"
}

title and the code extension carry the machine-readable code; detail is whatever the translator had to say, and is left out entirely when it had nothing. A ValidationError adds every individual failure, each described on its own — an entry of errors uses the same two member names as the root document, because it is the same thing one level down:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "validation.general",
  "status": 400,
  "code": "validation.general",
  "errors": [
    { "code": "course.title.required", "detail": "Title is required" },
    { "code": "course.title.tooLong", "detail": "Title is longer than 200 characters" }
  ]
}

Error.Metadata never reaches the response — it is internal diagnostics.

Endpoints

Each endpoint declares its own route, next to its handler:

using i26.AspNetCore.Endpoints;

internal sealed class PublishCourse : IEndpoint
{
    public void MapEndpoint(IEndpointRouteBuilder app)
    {
        app.MapPost("courses/{id}/publish", async (
                [FromRoute] CourseId id,
                [FromServices] ICommandHandler<PublishCourseCommand, Course> handler,
                CancellationToken ct) =>
            {
                var result = await handler.HandleAsync(new PublishCourseCommand(id), ct);

                return result.Match(Results.Ok, ProblemResults.Problem);
            })
            .RequireAuthorization()
            .WithTags("Courses")
            .ProducesProblem(CourseErrors.NotFound, CourseErrors.AlreadyPublished);
    }
}

AddEndpoints finds them, MapEndpoints maps them onto whatever builder you call it on — the application, or a group whose prefix and conventions they all inherit. Forgetting AddEndpoints throws instead of quietly starting an API with no routes.

Declaring what can go wrong

ProducesProblem puts the statuses in the OpenAPI document, taking the errors themselves so the document follows the code:

.ProducesProblem(CourseErrors.NotFound, CourseErrors.AlreadyPublished)   // 404 and 409
.ProducesProblem(ErrorType.Unauthorized)                                 // or by kind of failure

Statuses are deduplicated, and it works on a route group as well as on a single endpoint.

Global exception handler

Anything that escapes a handler comes back in the same shape as a business failure:

  • a BadHttpRequestException — malformed JSON, a value that would not bind — becomes 400 with the code request.{field}.invalid, the field taken from the JSON path and passed as an argument;
  • anything else becomes 500 with the code general.failure.

The exception message reaches the client only in Development. Anywhere else a 500 carries nothing but its code — messages routinely spell out connection strings, file paths and SQL. The full exception is always in the log.

Three codes to add to your resources: general.failure, request.body.invalid and request.{field}.invalid.


A namespace called Results

Naming note. If your project has a namespace ending in Results — say Api.Results — as a sibling of the one holding your endpoints, the identifier Results in those files resolves to that namespace instead of Microsoft.AspNetCore.Http.Results. Alias it, and put the alias inside the namespace declaration:

namespace Api.Endpoints.Courses;

using Results = Microsoft.AspNetCore.Http.Results;

Above the declaration it would sit at compilation-unit scope, which is searched last and loses to the enclosing namespace's member. TypedResults sidesteps the name, but its concrete return types do not unify with ProblemResults.Problem in a single Match, so that fold would have to be typed as IResult by hand.


What it drags in

Nothing from NuGet: it uses the ASP.NET Core shared framework, plus i26.Core.


Part of i26.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.5.0 118 8/13/2026
0.4.0 112 8/13/2026
0.3.1 105 8/13/2026
0.3.0 107 8/12/2026
0.2.0 106 8/12/2026
0.1.0 101 8/12/2026