Reprise 3.3.0
See the version list below for details.
dotnet add package Reprise --version 3.3.0
NuGet\Install-Package Reprise -Version 3.3.0
<PackageReference Include="Reprise" Version="3.3.0" />
paket add Reprise --version 3.3.0
#r "nuget: Reprise, 3.3.0"
// Install Reprise as a Cake Addin #addin nuget:?package=Reprise&version=3.3.0 // Install Reprise as a Cake Tool #tool nuget:?package=Reprise&version=3.3.0
𝄇 Reprise
reprise /rɪˈpriːz/ noun [French] A repeated part of something, especially a piece of music
Reprise is a micro-framework that brings the REPR (Request-Endpoint-Response) pattern and vertical slice architecture into the ASP.NET Core 6/7 Minimal APIs. It aims to be unopioniated towards API behavior and to provide a thin layer of abstractions that encourages the creation of convention-based and modular implementations.
Getting started
Create a new ASP.NET Core 6/7 empty project.
Install the Reprise NuGet package.
Add
Reprise
as a global using.Modify your
Program.cs
to use Reprise.
var builder = WebApplication.CreateBuilder();
builder.ConfigureServices();
var app = builder.Build();
app.UseExceptionHandling();
app.MapEndpoints();
app.Run();
- Create an endpoint
Endpoints/GetHelloEndpoint.cs
.
[Endpoint]
public class GetHelloEndpoint
{
[Get("/")]
public static string Handle() => "Hello, world!";
}
- Run the application.
Endpoints
An endpoint is a public class that is decorated with the EndpointAttribute
.
It should contain a public static Handle
method which should be decorated
with one of the HTTP method and route attributes. These are:
GetAttribute
PostAttribute
PutAttribute
PatchAttribute
DeleteAttribute
MapAttribute
- for other/multiple HTTP methods
The Handle
method can be synchronous as well as asynchronous and can have
any signature and any return type.
[Endpoint]
public class UpdateUserEndpoint
{
[Put("/users/{id}")]
public static IResult Handle(int id, UserDto userDto, DataContext context)
{
// ...
}
}
In the example id
comes from the route, userDto
- from the body, and
context
- from the DI container. Check
the documentation
for more information about parameter binding.
Services
Any class (including an endpoint) that has a public parameterless constructor
and implements IServiceConfgurator
can configure services.
[Endpoint]
public class GetWeatherEndpoint : IServiceConfigurator
{
public void ConfigureServices(WebApplicationBuilder builder)
{
builder.Services.AddWeatherService();
}
[Get("/weather")]
public static async Task<WeatherForecast[]> Handle(IWeatherService weatherService)
{
return await weatherService.GetForecast();
}
}
Configuration
You can bind a strongly typed hierarchical configuration using the ConfigurationAttribute
.
A simple example would look like this:
[Endpoint]
public class GetGreetingsEndpoint
{
[Get("/greetings")]
public static IEnumerable<string> Handle(GreetingConfiguration configuration)
{
return configuration.Names.Select(name => string.Format(configuration.Message, name));
}
}
[Configuration("Greeting")]
public class GreetingConfiguration
{
public string Message { get; set; } = null!;
public List<string> Names { get; set; } = null!;
}
The GreetingConfiguration
is added to the DI container with a singleton lifetime and
is bound to the following section in appsettings.json
:
{
"Greeting": {
"Message": "Hello, {0}!",
"Names": [ "Alice", "John", "Skylar" ]
}
}
Deeper nested sub-sections could as well be bound using a key like "Foo:Bar"
. Check
the documentation
for more information about configuration.
Filters (.NET 7 only)
Reprise supports global and endpoint-specific filters that implement IEndpointFilter
.
The filter execution order is determined by an order value that is set implicitly or explicitly. A higher value means the filter will be executed after the ones with a lower value. Filters having equal order values are run in an arbitrary order.
Global filters are implicitly assigned an incrementing order value starting from 0
according to the registration order.
app.MapEndpoints(options =>
{
options.AddEndpointFilter<FilterA>(); // implicit order 0
options.AddEndpointFilter<FilterB>(); // implicit order 1
options.AddEndpointFilter<FilterC>(); // implicit order 2
options.AddEndpointFilter<FilterD>(100); // explicit order 100
});
Endpoint-specific filters are implicitly assigned an order value of int.MaxValue
.
[Endpoint]
public class DeleteUserEndpoint
{
[Delete("/users/{id}")]
[Filter(typeof(FilterE))] // implicit order int.MaxValue
[Filter(typeof(FilterF))] // implicit order int.MaxValue
[Filter(typeof(FilterG), -1)] // explicit order -1
public static IResult Handle(int id, DataContext context)
{
// ...
}
}
Check the documentation for more information about filters.
Validation
Reprise relies on FluentValidation. On application startup, all IValidator<T>
implementations are
added with a singleton lifetime. You can then inject the validator in your Handle
method.
[Endpoint]
public class CreateUserEndpoint
{
[Post("/users")]
public static IResult Handle(UserDto userDto, IValidator<UserDto> validator, DataContext context)
{
validator.ValidateAndThrow(userDto);
// ...
}
}
public class UserDtoValidator : AbstractValidator<UserDto>
{
public UserDtoValidator()
{
RuleFor(u => u.FirstName).NotEmpty();
RuleFor(u => u.LastName).NotEmpty();
}
}
Check the documentation for more information about FluentValidation.
Validation filter (.NET 7 only)
It's also possible to add a global validation filter. This way, you don't need to manually inject validators and validate.
app.MapEndpoints(options => options.AddValidationFilter());
The filter is registered using an endpoint filter factory, so it will be invoked only on endpoints having one or more parameters that are validatable.
Exception handling
By default, the exception handler returns no body and:
- On
BadHttpRequestException
(e.g. thrown when the request body couldn't be deserialized) logs an error and returns status code400
. - On
ValidationException
returns status code400
. - On all other exceptions logs an error and returns status code
500
.
To customize exception logging, you can implement IExceptionLogger
.
Similarly, you can customize the response body by implementing IErrorResponseFactory
.
Authorization
There are two ways to secure your API. You can require authorization only for specific
endpoints by decorating the Handle
method with the AuthorizeAttribute
from
Microsoft.AspNetCore.Authorization
. Alternatively, you can require authorization
for all endpoints and opt-out for specific ones using the AllowAnonymousAttribute
.
app.MapEndpoints(options => options.RequireAuthorization());
Check the documentation for more information about authentication and authorization.
CORS
The CORS middleware and attributes work with Reprise exactly the same way as when using pure Minimal APIs.
Check the documentation for more information about CORS.
OpenAPI
You can enhance the OpenAPI description of your endpoints by decorating the Handle
method with attributes.
TagsAttribute
assigns custom tags that are typically used to group operations in the Swagger UI. If no attribute is found, Reprise extracts a tag from the route of every endpoint. The first segment that is not "api" (case insensitive) or a parameter is capitalized and set as a tag. "/" is used when no match is found.NameAttribute
assigns a name that is used for link generation and is also treated as the operation ID in the OpenAPI description.ProducesAttribute
describes a response returned from an API endpoint. NB: Make sure you are using the Reprise' attribute and not the one fromMicrosoft.AspNetCore.Mvc
.ExcludeFromDescriptionAttribute
marks an endpoint that is excluded from the OpenAPI description.
Check the documentation for more information about OpenAPI.
Events
Events provide an elegant way to do some work using the publish-subscribe pattern. An event is handled by one or more handlers in parallel. All event handlers are added to the DI container with a scoped lifetime.
public record Greeting(string Message) : IEvent;
public class GreetingHandler : IEventHandler<Greeting>
{
private readonly ILogger<GreetingHandler> _Logger;
public GreetingHandler(ILogger<GreetingHandler> logger)
{
_Logger = logger;
}
public async Task Handle(Greeting payload, CancellationToken cancellationToken)
{
await Task.Delay(1_000, cancellationToken);
_Logger.LogInformation("Received greeting with message: {Message}", payload.Message);
}
}
You can publish events via the IEventBus
. It has two methods:
Publish
does not wait for the handlers to finish execution. All matching handlers are resolved in a newIServiceScope
.IHostApplicationLifetime.ApplicationStopping
is passed to the handlers as a cancellation token. If a handler throws, the exception is logged.PublishAndWait
waits for all handlers to finish execution. All matching handlers are resolved in the currentIServiceScope
. If one or more handlers throw, all exceptions are packed into anAggregateException
.
Self-checks
Reprise tries to make sure the API will behave exactly the way you would expect. For this reason,
it performs various self-checks on application startup and throws an InvalidOperationException
when a problem is encountered. The problems covered by those checks include:
- Programming errors (e.g., an endpoint has no public static
Handle
method) - Misconfigurations (e.g., a configuration model could not be bound)
- Code duplications (e.g., a model is validated by multiple validators)
- Ambiguities (e.g., an HTTP method and route combination is handled by more than one endpoint).
Performance
Besides the assembly scan at application startup when configuring the DI container and discovering endpoints, Reprise doesn't add any performance overhead when handling requests.
Startup
Method | Mean | Error | StdDev | Median | Gen0 | Gen1 | Allocated |
---|---|---|---|---|---|---|---|
Reprise | 5.249 ms | 0.4375 ms | 1.2901 ms | 4.896 ms | 31.2500 | 31.2500 | 302.22 KB |
Carter | 4.193 ms | 0.2483 ms | 0.7321 ms | 4.032 ms | 31.2500 | 31.2500 | 268.96 KB |
FastEndpoints | 4.901 ms | 0.3249 ms | 0.9580 ms | 4.797 ms | 31.2500 | 31.2500 | 319.60 KB |
MinimalApis | 4.111 ms | 0.2300 ms | 0.6781 ms | 3.998 ms | 31.2500 | 31.2500 | 272.38 KB |
Request
Method | Mean | Error | StdDev | Gen0 | Allocated |
---|---|---|---|---|---|
Reprise | 112.1 μs | 0.60 μs | 0.53 μs | 5.6152 | 17.13 KB |
Carter | 115.2 μs | 1.28 μs | 1.20 μs | 5.3711 | 16.63 KB |
FastEndpoints | 123.5 μs | 2.41 μs | 2.68 μs | 5.8594 | 17.91 KB |
MinimalApis | 114.9 μs | 0.85 μs | 0.80 μs | 5.3711 | 16.67 KB |
Further reading
Support
If you spot any problems and/or have improvement ideas, please share them via the issue tracker.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. 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. |
-
net6.0
- FluentValidation (>= 11.4.0)
-
net7.0
- FluentValidation (>= 11.4.0)
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 |
---|---|---|
3.7.0 | 394 | 7/16/2023 |
3.6.0 | 205 | 7/12/2023 |
3.5.0 | 267 | 4/30/2023 |
3.4.2 | 367 | 2/20/2023 |
3.4.1 | 300 | 2/16/2023 |
3.4.0 | 302 | 2/5/2023 |
3.3.1 | 254 | 2/3/2023 |
3.3.0 | 288 | 2/1/2023 |
3.2.0 | 318 | 1/20/2023 |
3.1.6 | 336 | 12/20/2022 |
3.1.5 | 312 | 12/18/2022 |
3.1.4 | 301 | 12/12/2022 |
3.1.3 | 306 | 12/11/2022 |
3.1.2 | 355 | 12/4/2022 |
3.1.1 | 345 | 11/30/2022 |
3.1.0 | 331 | 11/27/2022 |
3.0.0 | 328 | 11/26/2022 |
2.2.0 | 357 | 11/23/2022 |
2.1.0 | 373 | 11/21/2022 |
2.0.0 | 348 | 11/19/2022 |
1.1.0 | 375 | 10/30/2022 |
1.0.3 | 425 | 10/22/2022 |
1.0.2 | 407 | 10/21/2022 |
1.0.1 | 427 | 10/20/2022 |
1.0.0 | 1,177 | 10/19/2022 |