Skip to main content

ASP.NET Core

The Spur.AspNetCore package converts Result<T> values into HTTP responses automatically — 200 OK for success, RFC 7807 Problem Details for errors.

Installation

dotnet add package Spur.AspNetCore

Setup

Register Spur services in Program.cs:

using Spur.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSpur();

var app = builder.Build();

With custom options

builder.Services.AddSpur(options =>
{
options.ProblemDetailsTypeBaseUri = "https://api.myapp.com/errors/";
options.IncludeErrorCode = true;
options.IncludeErrorCategory = true;
options.IncludeInnerErrors = false; // hide inner errors in production
options.IncludeCustomExtensions = true;
});

SpurOptions reference

PropertyDefaultDescription
ProblemDetailsTypeBaseUri"https://errors.example.com/"Base URI for the type field in Problem Details
IncludeErrorCodetrueInclude errorCode in the response
IncludeErrorCategorytrueInclude category in the response
IncludeInnerErrorstrueInclude inner error chain
IncludeCustomExtensionstrueInclude custom extensions
CustomStatusMappernullOverride HTTP status code mapping

Minimal APIs

ToHttpResult

Convert a Result<T> to an IResult:

app.MapGet("/users/{id}", async (int id, IUserRepository repo) =>
{
return await repo.GetByIdAsync(id)
.ToHttpResult();
});
  • Success → 200 OK with the value as JSON
  • Failure → Problem Details with the error's HTTP status code

Custom success status code

app.MapPost("/users", async (CreateUserRequest req, IUserRepository repo) =>
{
return await repo.CreateAsync(req)
.ToHttpResult(statusCode: 201); // 201 Created
});

Created with location

app.MapPost("/users", async (CreateUserRequest req, IUserRepository repo) =>
{
return await repo.CreateAsync(req)
.ToHttpResultCreated($"/users/{req.Email}");
});

Void operations (Result<Unit>)

When the Result has no meaningful value, a success returns 204 No Content:

app.MapDelete("/users/{id}", async (int id, IUserRepository repo) =>
{
return await repo.DeleteAsync(id)
.ToHttpResult(); // 204 No Content on success
});

MVC Controllers

Use ToActionResult in controllers:

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IUserRepository _repo;
private readonly IProblemDetailsMapper _mapper;

[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
return await _repo.GetByIdAsync(id)
.ToActionResult(_mapper);
}

[HttpPost]
public async Task<IActionResult> Create(CreateUserRequest request)
{
return await _repo.CreateAsync(request)
.ToActionResult(_mapper, successStatusCode: 201);
}
}

SpurMiddleware

Optionally add middleware that catches unhandled exceptions and converts them to Problem Details:

app.UseMiddleware<SpurMiddleware>();

This catches:

  • SpurException → maps the inner Error to Problem Details
  • Any other Exception → wraps as Error.Unexpected500 Problem Details

Problem Details response format

Every error produces an RFC 7807-compliant response:

{
"type": "https://errors.example.com/USER_NOT_FOUND",
"title": "Not Found",
"status": 404,
"detail": "User 123 not found",
"errorCode": "USER_NOT_FOUND",
"category": "NotFound"
}

Custom ProblemDetailsMapper

Implement IProblemDetailsMapper for full control:

public class MyMapper : IProblemDetailsMapper
{
public ProblemDetails ToProblemDetails(Error error)
{
return new ProblemDetails
{
Type = $"https://api.myapp.com/errors/{error.Code}",
Title = error.Category.ToString(),
Status = error.HttpStatus,
Detail = error.Message
};
}
}

// Register
builder.Services.AddSpur<MyMapper>();

See also