Futurum.WebApiEndpoint.Micro
1.0.2
See the version list below for details.
dotnet add package Futurum.WebApiEndpoint.Micro --version 1.0.2
NuGet\Install-Package Futurum.WebApiEndpoint.Micro -Version 1.0.2
<PackageReference Include="Futurum.WebApiEndpoint.Micro" Version="1.0.2" />
paket add Futurum.WebApiEndpoint.Micro --version 1.0.2
#r "nuget: Futurum.WebApiEndpoint.Micro, 1.0.2"
// Install Futurum.WebApiEndpoint.Micro as a Cake Addin #addin nuget:?package=Futurum.WebApiEndpoint.Micro&version=1.0.2 // Install Futurum.WebApiEndpoint.Micro as a Cake Tool #tool nuget:?package=Futurum.WebApiEndpoint.Micro&version=1.0.2
Futurum.WebApiEndpoint.Micro
A dotnet library that allows you to build WebApiEndpoints using a vertical slice architecture approach. Built on dotnet 7 and minimal apis.
- Vertical Slice Architecture, gives you the ability to add new features without changing existing code
- Easy setup
- Full support and built on top of minimal apis
- Full support for OpenApi
- Full support for TypedResults
- Full compatibility with Futurum.Core
- Supports uploading file(s) with additional JSON payload
- Api Versioning baked-in
- Built in Validation support
- Built on dotnet 7
- Built in use of ProblemDetails support
- Tested solution
- Comprehensive samples
- Convention Customisation
- Autodiscovery of WebApiEndpoint, based on Source Generators
What is a WebApiEndpoint?
- It's a vertical slice / feature of your application
- The vertical slice is a self-contained unit of functionality
- Collection of WebApis that share a route prefix and version
Easy setup
- Add the NuGet package ( futurum.webapiendpoint.micro ) to your project
- Update program.cs as per here
- Create a new class that implements IWebApiEndpoint
- Add the WebApiEndpoint attribute to the class, if you want to specify a specific route prefix and tag
- Add the WebApiEndpointVersion attribute to the class, if you want to specify a specific ApiVersion
- Implement the Register and add minimal api(s) as per usual
program.cs
AddWebApiEndpoints
Allows you to configure:
- DefaultApiVersion (mandatory)
- This is used if a specific ApiVersion is not provided for a specific WebApiEndpoint
- DefaultOpenApiInfo (optional)
- This is used if a specific OpenApiInfo is not provided for a specific ApiVersion
- OpenApiDocumentVersions (optional)
- Allowing you to have different OpenApiInfo per ApiVersion
- VersionPrefix (optional)
- VersionFormat (optional)
- uses 'Asp.Versioning.ApiVersionFormatProvider'
builder.Services.AddWebApiEndpoints(new WebApiEndpointConfiguration(WebApiEndpointVersions.V1_0)
{
OpenApiDocumentVersions =
{
{
WebApiEndpointVersions.V1_0,
new OpenApiInfo
{
Title = "Futurum.WebApiEndpoint.Micro.Sample v1"
}
}
}
});
AddWebApiEndpointsFor... (per project containing WebApiEndpoints)
This will be automatically created by the source generator.
e.g.
builder.Services.AddWebApiEndpointsForFuturumWebApiEndpointMicroSample();
UseWebApiEndpoints
Adds the WebApiEndpoints to the pipeline
app.UseWebApiEndpoints();
UseWebApiEndpointsOpenApi
Register the OpenApi UI (Swagger and SwaggerUI) middleware
app.UseWebApiEndpointsOpenApi();
Full example
using Futurum.WebApiEndpoint.Micro;
using Futurum.WebApiEndpoint.Micro.Sample;
using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddWebApiEndpoints(new WebApiEndpointConfiguration(WebApiEndpointVersions.V1_0)
{
DefaultOpenApiInfo = new OpenApiInfo
{
Title = "Futurum.WebApiEndpoint.Micro.Sample",
}
})
.AddWebApiEndpointsForFuturumWebApiEndpointMicroSample();
var app = builder.Build();
app.UseWebApiEndpoints();
if (app.Environment.IsDevelopment())
{
app.UseWebApiEndpointsOpenApi();
}
app.Run();
IWebApiEndpoint
Register
You can map your minimal apis for this WebApiEndpoint in the Register method.
The builder parameter is already:
- configured with the API versioning
- configured with the route prefix
- gone through the Configure method in the same class (if there is one)
public void Register(IEndpointRouteBuilder builder)
{
}
Full example
Weather
[WebApiEndpoint("weather")]
public class WeatherWebApiEndpoint : IWebApiEndpoint
{
private static readonly string[] Summaries =
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public void Register(IEndpointRouteBuilder builder)
{
builder.MapGet("/", GetHandler);
}
private static Ok<IEnumerable<WeatherForecastDto>> GetHandler(HttpContext httpContext, CancellationToken cancellationToken) =>
Enumerable.Range(1, 5)
.Select(index => new WeatherForecastDto(DateOnly.FromDateTime(DateTime.Now.AddDays(index)), Random.Shared.Next(-20, 55), Summaries[Random.Shared.Next(Summaries.Length)]))
.ToOk();
}
File download
[WebApiEndpoint("bytes", "feature")]
public class BytesWebApiEndpoint : IWebApiEndpoint
{
public void Register(IEndpointRouteBuilder builder)
{
builder.MapGet("download", DownloadHandler);
}
private static Results<NotFound, FileContentHttpResult, BadRequest<ProblemDetails>> DownloadHandler(HttpContext context)
{
return Result.Try(Execute, () => "Failed to read file")
.ToWebApi(context);
Results<NotFound, FileContentHttpResult> Execute()
{
var path = "./Data/hello-world.txt";
if (!File.Exists(path))
{
return TypedResults.NotFound();
}
var bytes = File.ReadAllBytes(path);
return TypedResults.Bytes(bytes, MediaTypeNames.Application.Octet, "hello-world.txt");
}
}
}
Configure
You can configure the WebApiEndpoint in the Configure method
public void Configure(RouteGroupBuilder groupBuilder, WebApiEndpointVersion webApiEndpointVersion)
{
}
This allows you to set properties on the RouteGroupBuilder.
You can also configure it differently per ApiVersion.
NOTE: this is optional
NOTE: this ia a good place to add EndpointFilter
public void Configure(RouteGroupBuilder groupBuilder, WebApiEndpointVersion webApiEndpointVersion)
{
groupBuilder.AddEndpointFilter<CustomEndpointFilter>();
}
NOTE: this ia a good place to add Security
public void Configure(RouteGroupBuilder groupBuilder, WebApiEndpointVersion webApiEndpointVersion)
{
groupBuilder.RequireAuthorization(Authorization.Permission.Admin);
}
Validation
ValidationService
Executes FluentValidation and DataAnnotations
IValidationService<ArticleDto> validationService
private static Results<Ok<ArticleDto>, ValidationProblem, BadRequest<ProblemDetails>> ValidationHandler(HttpContext context, IValidationService<ArticleDto> validationService,
ArticleDto articleDto) =>
validationService.Execute(articleDto)
.Map(() => new Article(null, articleDto.Url))
.Map(ArticleMapper.MapToDto)
.ToWebApi(context, ToOk, ToValidationProblem);
FluentValidationService
Calls FluentValidation
IFluentValidationService<ArticleDto> fluentValidationService
e.g.
private static Results<Ok<ArticleDto>, ValidationProblem, BadRequest<ProblemDetails>> FluentValidationHandler(HttpContext context, IFluentValidationService<ArticleDto> fluentValidationService,
ArticleDto articleDto) =>
fluentValidationService.Execute(articleDto)
.Map(() => new Article(null, articleDto.Url))
.Map(ArticleMapper.MapToDto)
.ToWebApi(context, ToOk, ToValidationProblem);
public class ArticleDtoValidator : AbstractValidator<ArticleDto>
{
public ArticleDtoValidator()
{
RuleFor(x => x.Url).NotEmpty().WithMessage("must have a value;");
}
}
DataAnnotationsValidationService
Calls DataAnnotations validation
IDataAnnotationsValidationService dataAnnotationsValidationService
private static Results<Ok<ArticleDto>, ValidationProblem, BadRequest<ProblemDetails>> DataAnnotationsValidationHandler(HttpContext context,
IDataAnnotationsValidationService dataAnnotationsValidationService,
ArticleDto articleDto) =>
dataAnnotationsValidationService.Execute(articleDto)
.Map(() => new Article(null, articleDto.Url))
.Map(ArticleMapper.MapToDto)
.ToWebApi(context, ToOk, ToValidationProblem);
Uploading file(s) with additional JSON payload
Upload single file and payload
Use the FormFileWithPayload type to upload a single file and a JSON payload
private static Task<Results<Ok<FileDetailsWithPayloadDto>, BadRequest<ProblemDetails>>> UploadWithPayloadHandler(HttpContext context, FormFileWithPayload<PayloadDto> fileWithPayload)
{
return Result.TryAsync(Execute, () => "Failed to read file")
.ToWebApiAsync(context, ToOk);
async Task<FileDetailsWithPayloadDto> Execute()
{
var tempFile = Path.GetTempFileName();
await using var stream = File.OpenWrite(tempFile);
await fileWithPayload.File.CopyToAsync(stream);
return new FileDetailsWithPayloadDto(fileWithPayload.File.FileName, fileWithPayload.Payload.Name);
}
}
Upload multiple files and payload
Use the FormFilesWithPayload type to upload multiple files and a JSON payload
private static Task<Results<Ok<IEnumerable<FileDetailsWithPayloadDto>>, BadRequest<ProblemDetails>>> UploadsWithPayloadHandler(
HttpContext context, FormFilesWithPayload<PayloadDto> filesWithPayload)
{
return Result.TryAsync(Execute, () => "Failed to read file")
.ToWebApiAsync(context, ToOk);
async Task<IEnumerable<FileDetailsWithPayloadDto>> Execute()
{
var fileDetails = new List<FileDetailsWithPayloadDto>();
foreach (var file in filesWithPayload.Files)
{
var tempFile = Path.GetTempFileName();
await using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
fileDetails.Add(new FileDetailsWithPayloadDto(file.FileName, filesWithPayload.Payload.Name));
}
return fileDetails;
}
}
Results<...> → Results<..., BadRequest<ProblemDetails>>
Comprehensive set of extension methods - WebApiEndpointRunner.Run and WebApiEndpointRunner.RunAsync - to run a method and if it throws an exception it will catch and transform it into a BadRequest<ProblemDetails>.
The Run and RunAsync methods will:
- If the method passed in does not throw an exception, then the existing return remains the same.
- If the method passed in does throw an exception, then a BadRequest<ProblemDetails> will be returned, with the appropriate details set on the ProblemDetails. The error message will be safe to return to the client, that is, it will not contain any sensitive information e.g. StackTrace.
The returned type from Run and RunAsync is always augmented to additionally include BadRequest<ProblemDetails>
T -> Results<T, BadRequest<ProblemDetails>>
Results<TIResult1, TIResult2> -> Results<TIResult1, TIResult2, BadRequest<ProblemDetails>>
Results<TIResult1, TIResult2, TIResult3> -> Results<TIResult1, TIResult2, TIResult3, BadRequest<ProblemDetails>>
Results<TIResult1, TIResult2, TIResult3, TIResult4> -> Results<TIResult1, TIResult2, TIResult3, TIResult4, BadRequest<ProblemDetails>>
Results<TIResult1, TIResult2, TIResult3, TIResult4, TIResult5> -> Results<TIResult1, TIResult2, TIResult3, TIResult4, TIResult5, BadRequest<ProblemDetails>>
Results has a maximum of 6 types. So 5 are allowed leaving one space left for the BadRequest<ProblemDetails>.
Example use
In this example the Execute method will return:
- a NotFound if the file does not exist
- a FileStreamHttpResult if the file exists
Results<NotFound, FileStreamHttpResult>
The Run / RunAsync extension method will change this to add BadRequest<ProblemDetails>.
Results<NotFound, FileStreamHttpResult, BadRequest<ProblemDetails>>
Full Example
private static Results<NotFound, FileStreamHttpResult, BadRequest<ProblemDetails>> DownloadHandler(HttpContext context)
{
return Run(Execute, context, "Failed to read file");
Results<NotFound, FileStreamHttpResult> Execute()
{
var path = "./Data/hello-world.txt";
if (!File.Exists(path))
{
return TypedResults.NotFound();
}
var fileStream = File.OpenRead(path);
return TypedResults.File(fileStream, MediaTypeNames.Application.Octet, "hello-world.txt");
}
}
Note: It is recommended to add the following to your GlobalUsings.cs file.
global using static Futurum.WebApiEndpoint.Micro.WebApiEndpointRunner;
This means you can use the helper functions without having to specify the namespace. As in the examples.
Full compatibility with Futurum.Core
Comprehensive set of extension methods to transform a Result and Result<T> to an TypedResult.
- If the method passed in is a success, then the IResult will be returned.
- If the method passed in is a failure, then a BadRequest<ProblemDetails> will be returned, with the appropriate details set on the ProblemDetails. The error message will be safe to return to the client, that is, it will not contain any sensitive information e.g. StackTrace.
The returned type from ToWebApi is always augmented to additionally include BadRequest<ProblemDetails>
Result<T> -> Results<T, BadRequest<ProblemDetails>>
Result<Results<TIResult1, TIResult2>> -> Results<TIResult1, TIResult2, BadRequest<ProblemDetails>>
Result<Results<TIResult1, TIResult2, TIResult3>> -> Results<TIResult1, TIResult2, TIResult3, BadRequest<ProblemDetails>>
Result<Results<TIResult1, TIResult2, TIResult3, TIResult4>> -> Results<TIResult1, TIResult2, TIResult3, TIResult4, BadRequest<ProblemDetails>>
Result<Results<TIResult1, TIResult2, TIResult3, TIResult4, TIResult5>> -> Results<TIResult1, TIResult2, TIResult3, TIResult5, BadRequest<ProblemDetails>>
Results has a maximum of 6 types. So 5 are allowed leaving one space left for the BadRequest<ProblemDetails>.
How to handle successful and failure cases in a typed way with TypedResult
You can optionally specify which TypedResult success cases you want to handle. This is useful if you want to handle a specific successes case differently.
You can specify which TypedResult error cases you want to handle. This is useful if you want to handle a specific error case differently.
If you have a success case, you must pass in the the success helper function first, then the failure helper functions.
There can only be 1 success helper function, but there can be multiple failure helper functions.
Example use
The ToWebApi extension method will change the method return type to add BadRequest<ProblemDetails>, with the appropriate details set on the ProblemDetails. The error message will be safe to return to the client, that is, it will not contain any sensitive information e.g. StackTrace.
You can then pass in additional helper functions to deal with successes and failures and these will change the return type to the appropriate TypedResult's.
ToOk is a function that will convert a T to an Ok<T>.
ToValidationProblem is a function that will convert a ValidationResultError to a ValidationProblem.
Full Example
private static Results<Ok<ArticleDto>, ValidationProblem, BadRequest<ProblemDetails>> ValidationHandler(HttpContext context, IValidationService<ArticleDto> validationService,
ArticleDto articleDto) =>
validationService.Execute(articleDto)
.Map(() => new Article(null, articleDto.Url))
.Map(ArticleMapper.MapToDto)
.ToWebApi(context, ToOk, ToValidationProblem);
Success and Failure helper functions
If you have a success case, you must pass in the the success helper function first, then the failure helper functions.
There can only be 1 success helper function, but there can be multiple failure helper functions.
Note: It is recommended to add the following to your GlobalUsings.cs file.
global using static Futurum.WebApiEndpoint.Micro.WebApiResultsExtensions;
This means you can use the helper functions without having to specify the namespace. As in the examples.
Success
ToOk
Converts a T to an Ok<T>.
ToOk
ToCreated
Converts a () to a Created.
ToCreated<string>
By default it will take the location from the HttpContext.Request.Path.
or
Converts a T to a Created<T>.
This can be overridden by passing in a string.
ToCreated<T>("/api/articles")
ToAccepted
Converts a () to a Accepted.
ToAccepted<string>
By default it will take the location from the HttpContext.Request.Path.
or
Converts a T to a Accepted<T>.
By default it will take the location from the HttpContext.Request.Path.
This can be overridden by passing in a string.
ToAccepted<T>("/api/articles")
Failure
ToNotFound
If a ResultErrorKeyNotFound has occured then it will convert it to a NotFound<ProblemDetails>, with the correct information set on the ProblemDetails.
ToNotFound
ToValidationProblem
If a ResultErrorValidation has occured then it will convert it to a ValidationProblem, with the correct information set on the HttpValidationProblemDetails.
ToValidationProblem
Comprehensive samples
There are examples showing the following:
- A basic blog CRUD implementation
- The ToDo sample from Damian Edwards here
- AsyncEnumerable
- Bytes file download
- EndpointFilter on a specific WebApiEndpoint
- Exception handling
- Result error handling
- File(s) upload
- File(s) upload with Payload
- File download
- OpenApi version support
- RateLimiting
- Security with a basic JWT example on a specific WebApiEndpoint
- Validation - DataAnnotations and FluentValidation and both combined
- Weather Forecast
- Addition project containing WebApiEndpoints
Convention Customisation
Although the default conventions are good enough for most cases, you can customise them.
IWebApiOpenApiVersionConfigurationService
This is used to get the OpenApiInfo for each WebApiEndpointVersion.
serviceCollection.AddWebApiEndpointOpenApiVersionConfigurationService<WebApiOpenApiVersionConfigurationService>();
IWebApiOpenApiVersionUIConfigurationService
This is used to configure the OpenApi JSON endpoint for each WebApiEndpointVersion.
serviceCollection.AddWebApiEndpointOpenApiVersionUIConfigurationService<WebApiOpenApiVersionUIConfigurationService>();
IWebApiVersionConfigurationService
This is used to configure ApiVersioning and ApiExplorer.
There is an overload of AddWebApiEndpoints that takes a generic type of IWebApiVersionConfigurationService.
builder.Services.AddWebApiEndpoints<CustomWebApiVersionConfigurationService>();
Use this instead
builder.Services.AddWebApiEndpoints();
IWebApiEndpointMetadataStrategy
This is used to get the metadata for each WebApiEndpoint.
The metadata contains:
- PrefixRoute
- Tag
- WebApiEndpointVersion
serviceCollection.AddWebApiEndpointMetadataStrategy<WebApiEndpointMetadataAttributeStrategy>();
The default strategy is WebApiEndpointMetadataAttributeStrategy.
WebApiEndpointMetadataAttributeStrategy
This uses the following attributes:
- WebApiEndpointAttribute - for 'PrefixRoute' and 'Tag'
- WebApiEndpointVersionAttribute - for 'WebApiEndpointVersion', can have multiple
[WebApiEndpoint("weather")]
[WebApiEndpointVersion(1)]
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | 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. |
-
net7.0
- Asp.Versioning.Http (>= 7.0.0)
- Asp.Versioning.Mvc.ApiExplorer (>= 7.0.0)
- FluentValidation (>= 11.5.1)
- Futurum.Core (>= 1.0.14)
- Futurum.Microsoft.Extensions.DependencyInjection (>= 1.0.3)
- Microsoft.AspNetCore.OpenApi (>= 7.0.4)
- Swashbuckle.AspNetCore (>= 6.5.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Futurum.WebApiEndpoint.Micro:
Package | Downloads |
---|---|
Futurum.WebApiEndpoint.Micro.Core.Extensions
A dotnet library that extends Futurum.WebApiEndpoint.Micro, to make it fully compatible with Futurum.Core. |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
2.0.8 | 352 | 12/26/2023 |
2.0.7 | 119 | 12/25/2023 |
2.0.6 | 183 | 12/14/2023 |
2.0.5 | 106 | 12/12/2023 |
2.0.4 | 121 | 12/10/2023 |
2.0.3 | 120 | 12/8/2023 |
2.0.2 | 133 | 12/6/2023 |
2.0.1 | 123 | 12/6/2023 |
2.0.0 | 128 | 12/6/2023 |
1.0.5 | 171 | 4/21/2023 |
1.0.4 | 180 | 4/14/2023 |
1.0.3 | 171 | 4/7/2023 |
1.0.2 | 201 | 4/2/2023 |
1.0.1 | 202 | 3/27/2023 |
1.0.0 | 209 | 3/26/2023 |