SGuard.AspNetCore 1.0.0

dotnet add package SGuard.AspNetCore --version 1.0.0
                    
NuGet\Install-Package SGuard.AspNetCore -Version 1.0.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="SGuard.AspNetCore" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SGuard.AspNetCore" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="SGuard.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 SGuard.AspNetCore --version 1.0.0
                    
#r "nuget: SGuard.AspNetCore, 1.0.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 SGuard.AspNetCore@1.0.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=SGuard.AspNetCore&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=SGuard.AspNetCore&version=1.0.0
                    
Install as a Cake Tool

SGuard.AspNetCore

CI NuGet License: MIT

Returns SGuard guard failures from ASP.NET Core as 400 Bad Request validation problem details (RFC 9457), for minimal APIs and controllers alike. Targets .NET 8, 9 and 10, works with trimming and Native AOT (CI publishes and runs a Native AOT app), and needs SGuard 3.0 or later. In a Native AOT app, check members directly instead of with a selector: SGuard's NullOrEmpty selectors don't support AOT.

app.MapPost("/orders", (CreateOrder command) =>
{
    ThrowIf.NullOrEmpty(command.CustomerId);
    ThrowIf.OutOfRange(command.Quantity, 1, 100);
    ThrowIf.NullOrEmpty(command, c => c.ShippingAddress!.City);
    // ...
});
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": { "Quantity": ["The value is outside the allowed range."] },
  "traceId": "00-84b3c6f1e0a5c2d9b1f3e7a4c6d8e0f2-1a2b3c4d5e6f7a8b-00"
}

📦 Installation

dotnet add package SGuard.AspNetCore
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSGuardProblemDetails();

var app = builder.Build();
app.UseExceptionHandler();

AddSGuardProblemDetails registers the problem details services and an IExceptionHandler; UseExceptionHandler turns the exception handler middleware on. Put it first in the pipeline, so it catches the exceptions of everything after it. If you call AddProblemDetails yourself, its options, such as CustomizeProblemDetails, apply to these responses too.

Exception handlers run in the order they are registered, and the first one that handles an exception wins. If you have a catch-all handler of your own, register it after AddSGuardProblemDetails:

builder.Services.AddSGuardProblemDetails();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>(); // gets everything SGuard.AspNetCore doesn't handle

🔒 What the client sees

A guard exception's message is written for you, the developer: it names the expression from your source code, such as command.Quantity, and includes the checked value when SGuardOptions.IncludeValuesInExceptions is on. None of that reaches the client, unless you copy it there yourself: a CustomizeProblemDetails callback or an IProblemDetailsWriter of your own can read the exception from ProblemDetailsContext.Exception.

  • The key is the member the guard checked: command.Quantity becomes Quantity, command.ShippingAddress!.City and the selector c => c.ShippingAddress!.City become ShippingAddress.City, and a parameter such as quantity stays quantity. Expressions that aren't a member, such as name.Trim(), and the collection guards ThrowIf.Any and ThrowIf.All use the empty key "", which is what ASP.NET Core uses for errors that don't belong to one field. The key comes from the guard's first argument, so pass the checked member first (ThrowIf.Equal(order.Status, OrderStatus.Closed)), and inside the request type's own methods write this.Address.City: the first part of a path is always taken as the variable that holds the object.
  • The message is fixed per guard and never includes a value:
Guard Message
Null, NullOrEmpty, NullOrWhiteSpace A value is required.
GreaterThan, GreaterThanOrEqual The value is too large.
LessThan, LessThanOrEqual The value is too small.
OutOfRange The value is outside the allowed range.
Between The value is in a range that isn't allowed.
Negative The value must not be negative.
NegativeOrZero The value must be greater than zero.
Zero The value must not be zero.
LongerThan / ShorterThan The value is too long. / The value is too short.
NotDefined The value isn't one of the allowed values.
Equal / NotEqual The value isn't allowed. / The value doesn't match the expected value.
Any, All The collection contains items that aren't allowed.

A client whose Accept header allows neither JSON nor */*, such as one that asks only for text/html, gets the status code with an empty body. Browsers send */* too, so they get the JSON.

⚙️ Options

builder.Services.AddSGuardProblemDetails(options =>
{
    // 422 Unprocessable Content instead of 400.
    options.StatusCode = StatusCodes.Status422UnprocessableEntity;

    // camelCase keys, to match the JSON of the request.
    options.ErrorKey = e => JsonNamingPolicy.CamelCase.ConvertName(SGuardProblemDetailsOptions.DefaultErrorKey(e));

    // Also handle your own exception, thrown by a guard's factory, and show its message, which you wrote for clients.
    options.Handle<OrderValidationException>();
    options.ErrorMessage = e => e is OrderValidationException
        ? e.Message
        : SGuardProblemDetailsOptions.DefaultErrorMessage(e);
});

camelCase here converts the whole path at once (ShippingAddress.City becomes shippingAddress.City); split it on . first to convert each part.

Which exceptions are handled

SGuard's guard exceptions, the types in SGuard.Exceptions, and the types you add with options.Handle<T>(), including the types derived from them. Everything else goes on to the next exception handler, and by default becomes a 500. That includes the BCL exceptions, such as an ArgumentNullException thrown by your own code or a library, and the InvalidOperationException SGuard throws when an exception factory returns null: they point to a bug, not to a bad request.

That also covers the ArgumentNullException that ThrowIf.LongerThan, ShorterThan, GreaterThan, LessThan, Between, Any and All throw when the value itself is null: it means the guard was called with nothing to check. When a request field can be missing, check it first, and the client gets a 400 for it:

ThrowIf.NullOrEmpty(command.Password);   // missing -> 400, "Password": "A value is required."
ThrowIf.ShorterThan(command.Password, 12); // short   -> 400, "Password": "The value is too short."

A guard that checks your application's own state rather than the request, such as a configuration value, throws a guard exception too, and would turn into a 400. Give such guards an exception of your own:

ThrowIf.NullOrEmpty(_options.ConnectionString, () => new InvalidOperationException("The connection string is missing."));

Logging

The handler logs each failure it handles at Debug level, with the exception type, the error key and the status code, but not the message. Separately, the exception handler middleware of ASP.NET Core 8 and 9 logs every exception at Error level, including those a handler returns as a 400, with the exception's message; ASP.NET Core 10 no longer logs exceptions that a handler handled. On 8 and 9, filter the Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware category if those entries are too noisy, and keep SGuardOptions.IncludeValuesInExceptions off if your logs must not hold request values.

License

MIT

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
1.0.0 89 10/16/2026