Skip to content

Reflectionless, source-generated, thin abstraction layer over the ASP.NET Core Minimal APIs interface

License

Notifications You must be signed in to change notification settings

JensDll/MinimalApiBuilder

Repository files navigation

MinimalApiBuilder

nuget

Reflectionless, source-generated, thin abstraction layer over the ASP.NET Core Minimal APIs interface.

How to Use

Based on the Vertical Slice Architecture with Feature folder. There is one class for every API endpoint. A basic example looks like the following:

using MinimalApiBuilder.Generator;

public partial class BasicEndpoint : MinimalApiBuilderEndpoint
{
    public static string Handle()
    {
        return "Hello, World!";
    }
}

The endpoint class must be partial, inherit from MinimalApiBuilderEndpoint, and have a static Handle or HandleAsync method. The endpoint is mapped through the typical IEndpointRouteBuilder Map<Verb> extension methods:

app.MapGet("/hello", BasicEndpoint.Handle);

This library depends on FluentValidation >= 11. An endpoint can have a validated request object:

public struct BasicRequest
{
    public required string Name { get; init; }
}

public partial class BasicRequestEndpoint : MinimalApiBuilderEndpoint
{
    public static string Handle([AsParameters] BasicRequest request)
    {
        return $"Hello, {request.Name}!";
    }
}

public class BasicRequestValidator : AbstractValidator<BasicRequest>
{
    public BasicRequestValidator()
    {
        RuleFor(static request => request.Name).MinimumLength(2);
    }
}

The incremental generator will generate code to validate the request object before the handler is called and return a ValidationProblem validation error result if the validation fails. To wire up the validation filters and to support the Request Delegate Generator, the Map methods need to be wrapped by the ConfigureEndpoints.Configure helper, which expects a comma-separated list of RouteHandlerBuilder:

using static MinimalApiBuilder.Generator.ConfigureEndpoints;

Configure(app.MapGet("/hello/{name}", BasicRequestEndpoint.Handle));

Validation in custom binding scenarios is also supported. For example, adapting the Microsoft BindAsync sample:

Show example
public record PagingData(string? SortBy, SortDirection SortDirection, int CurrentPage)
{
    private const string SortByKey = "sortby";
    private const string SortDirectionKey = "sortdir";
    private const string PageKey = "page";

    public static ValueTask<PagingData?> BindAsync(HttpContext httpContext)
    {
        ProductsEndpoint endpoint =
            httpContext.RequestServices.GetRequiredService<ProductsEndpoint>();

        SortDirection sortDirection = default;
        int page = default;

        if (httpContext.Request.Query.TryGetValue(SortDirectionKey,
            out StringValues sortDirectionValues))
        {
            if (!Enum.TryParse(sortDirectionValues, ignoreCase: true, out sortDirection))
            {
                endpoint.AddValidationError(SortDirectionKey,
                    "Invalid sort direction. Valid values are 'default', 'asc', or 'desc'.");
            }
        }
        else
        {
            endpoint.AddValidationError(SortDirectionKey, "Missing sort direction.");
        }

        if (httpContext.Request.Query.TryGetValue(PageKey, out StringValues pageValues))
        {
            if (!int.TryParse(pageValues, out page))
            {
                endpoint.AddValidationError(PageKey, "Invalid page number.");
            }
        }
        else
        {
            endpoint.AddValidationError(PageKey, "Missing page number.");
        }

        if (endpoint.HasValidationError)
        {
            return ValueTask.FromResult<PagingData?>(null);
        }

        PagingData result = new(httpContext.Request.Query[SortByKey], sortDirection, page);

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

public partial class ProductsEndpoint : MinimalApiBuilderEndpoint
{
    public static string Handle(PagingData pageData)
    {
        return pageData.ToString();
    }
}
Configure(app.MapGet("/products", ProductsEndpoint.Handle));

Unfortunately, TryParse cannot be validated this way as there is no easy way to access the IServiceProvider right now. To not short-circuit execution by throwing an exception when returning null from BindAsync, ThrowOnBadRequest needs to be disabled:

builder.Services.Configure<RouteHandlerOptions>(static options =>
{
    options.ThrowOnBadRequest = false;
});

Endpoints and validators need to be registered with dependency injection. The following method adds them:

builder.Services.AddMinimalApiBuilderEndpoints();

Configuration

Users can add configuration through entries in .editorconfig or with MSBuild properties. The following options are available, with configuration snippets showing the default values:

minimalapibuilder_assign_name_to_endpoint (true | false)

If true, the generator will add a unique public const string Name field to the endpoint classes and call the WithName extension method when mapping them.

minimalapibuilder_assign_name_to_endpoint = false
<PropertyGroup>
  <minimalapibuilder_assign_name_to_endpoint>false</minimalapibuilder_assign_name_to_endpoint>
</PropertyGroup>

minimalapibuilder_validation_problem_type (string)

The type of the ValidationProblem validation error result.

minimalapibuilder_validation_problem_type = https://tools.ietf.org/html/rfc9110#section-15.5.1
<PropertyGroup>
  <minimalapibuilder_validation_problem_type>https://tools.ietf.org/html/rfc9110#section-15.5.1</minimalapibuilder_validation_problem_type>
</PropertyGroup>

minimalapibuilder_validation_problem_title (string)

The title of the ValidationProblem validation error result.

minimalapibuilder_validation_problem_title = One or more validation errors occurred.
<PropertyGroup>
  <minimalapibuilder_validation_problem_title>One or more validation errors occurred.</minimalapibuilder_validation_problem_title>
</PropertyGroup>

minimalapibuilder_model_binding_problem_type (string)

The type of the ValidationProblem model binding error result.

minimalapibuilder_model_binding_problem_type = https://tools.ietf.org/html/rfc9110#section-15.5.1
<PropertyGroup>
  <minimalapibuilder_model_binding_problem_type>https://tools.ietf.org/html/rfc9110#section-15.5.1</minimalapibuilder_model_binding_problem_type>
</PropertyGroup>

minimalapibuilder_model_binding_problem_title (string)

The title of the ValidationProblem model binding error result.

minimalapibuilder_model_binding_problem_title = One or more model binding errors occurred.
<PropertyGroup>
  <minimalapibuilder_model_binding_problem_title>One or more model binding errors occurred.</minimalapibuilder_model_binding_problem_title>
</PropertyGroup>

About

Reflectionless, source-generated, thin abstraction layer over the ASP.NET Core Minimal APIs interface

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages