Skip to content

A dotnet library that allows you to build WebApiEndpoints using a vertical slice architecture approach. Built on dotnet 8 and minimal apis.

License

Notifications You must be signed in to change notification settings

futurum-dev/dotnet.futurum.webapiendpoint.micro

Repository files navigation

Futurum.WebApiEndpoint.Micro

license CI Coverage Status NuGet version

The Futurum.WebApiEndpoint.Micro is a powerful .NET library that aids in the construction of WebApiEndpoints in a systematic manner.

It leverages the capabilities of .NET 8 and minimal APIs to facilitate the development of your Web APIs in a well-structured and defined manner. This library not only streamlines the process of Web API development but also provides the flexibility to incorporate new features without modifying any existing code. This is achieved through the implementation of a vertical slice architecture.

In the context of vertical slice architecture, each feature or functionality of the application is developed as an independent slice, from the user interface to the data storage layer. This approach enhances the modularity of the application, making it easier to add, modify, or remove features without affecting the rest of the application. The Futurum.WebApiEndpoint.Micro library embodies this architectural style, making it an excellent tool for developing robust and scalable Web APIs.

[WebApiEndpoint("greeting")]
public partial class GreetingWebApiEndpoint
{
    protected override void Build(IEndpointRouteBuilder builder)
    {
        builder.MapGet("/hello", HelloHandler);
        builder.MapGet("/goodbye", GoodbyeHandler);
    }

    private static Ok<string> HelloHandler(HttpContext context, string name) =>
        $"Hello {name}".ToOk();

    private static Ok<string> GoodbyeHandler(HttpContext context, string name) =>
        $"Goodbye {name}".ToOk();
}

Key Features

Table of Contents

  1. What is a WebApiEndpoint?
  2. Easy setup
  3. Configuration
  4. Sandbox runner
  5. Uploading file(s) with additional JSON payload
  6. Additional helper functions
  7. Comprehensive samples
  8. Convention Customisation
  9. Extendable GlobalExceptionHandler
  10. Tips & Tricks
  11. FAQ
  12. Troubleshooting
  13. Roslyn Analysers

What is a WebApiEndpoint?

  • It represents a vertical slice or a distinct feature of your application.
  • Each vertical slice is a self-contained functional unit.
  • It's a collection of Web APIs that share a common route prefix and version. They can also share various aspects such as Security, EndpointFilters, RateLimiting, OutputCaching, and more.

Easy setup

Check out this section for a step-by-step guide to setting up the library for use in your development environment.

  • ✅ Add the NuGet package ( futurum.webapiendpoint.micro ) to your project
  • ✅ Update program.cs as per here

Example program.cs

Here's an example of how to update your program.cs file:

using Futurum.WebApiEndpoint.Micro;
using Futurum.WebApiEndpoint.Micro.Sample;

var builder = WebApplication.CreateBuilder(args);

builder.Services
       .AddWebApiEndpoints(new WebApiEndpointConfiguration
       {
           DefaultApiVersion = WebApiEndpointVersions.V1_0.Version,
           OpenApi = new()
           {
               DefaultInfo = new()
               {
                   Title = "Futurum.WebApiEndpoint.Micro.Sample",
               }
           }
       })
       .AddWebApiEndpointsForFuturumWebApiEndpointMicroSample();

var app = builder.Build();

app.UseWebApiEndpoints();

if (app.Environment.IsDevelopment())
{
    app.UseWebApiEndpointsOpenApi();
}

app.Run();

See program.cs in sample project

AddWebApiEndpointsFor... (per project containing WebApiEndpoints)

This will be automatically created by the source generator.

You need to call this for each project that contains WebApiEndpoints, in order for them to be added to the pipeline.

e.g.

builder.Services.AddWebApiEndpointsForFuturumWebApiEndpointMicroSample();

UseWebApiEndpoints

Adds the WebApiEndpoints to the pipeline and does various other setup needed for the WebApiEndpoints to work.

app.UseWebApiEndpoints();

UseWebApiEndpointsOpenApi

Register the OpenApi UI (Swagger and SwaggerUI) middleware. This is usually only done in development mode.

app.UseWebApiEndpointsOpenApi();

How to create a WebApiEndpoint

  1. Create a new partial class.
  2. Add the WebApiEndpoint attribute to the class, with the route prefix for all the REST methods in this WebApiEndpoint. You can also optionally add a tag. This is used in the OpenApi documentation. If you do not specify a tag, then the route prefix is used.
  3. Add the WebApiEndpointVersion attribute to the class, if you want to specify a specific ApiVersion. If you do not specify a specific ApiVersion, then the default ApiVersion is used. You can add multiple WebApiEndpointVersion attributes to the class, if you want to support multiple ApiVersions.
  4. Implement the Build method and add minimal api(s) as per usual.
  5. Optionally implement the Configure method to configuration the WebApiEndpoint

Build

You can map your minimal apis for this WebApiEndpoint in the Build method.

The IEndpointRouteBuilder that the Build method receives has already:

protected override void Build(IEndpointRouteBuilder builder)
{
}
Full example
[WebApiEndpoint("weather")]
public partial class WeatherWebApiEndpoint
{
    private static readonly string[] Summaries =
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    protected override void Build(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();
}

See WeatherWebApiEndpoint in sample project

Configure

You can optionally configure the WebApiEndpoint in the Configure method.

protected override RouteGroupBuilder Configure(RouteGroupBuilder groupBuilder, WebApiEndpointVersion webApiEndpointVersion)
{
}

This allows you to setup the RouteGroupBuilder. This will effect all minimal apis in this classes Build method.

You can also configure it differently per ApiVersion.

This ia a good place to add a WebApiEndpoint specific EndpointFilter

groupBuilder.AddEndpointFilter<CustomEndpointFilter>();

See EndpointFilterWebApiEndpoint in sample project

This ia a good place to add a WebApiEndpoint specific RateLimiting

groupBuilder.RequireRateLimiting(RateLimiting.SlidingWindow.Policy);

See RateLimitingWebApiEndpoint in sample project

This ia a good place to add a WebApiEndpoint specific OutputCache

groupBuilder.CacheOutput(OutputCaching.ExpiryIn10Seconds.Policy);

See OutputCachingWebApiEndpoint in sample project

This ia a good place to add WebApiEndpoint specific Security

groupBuilder.RequireAuthorization(Authorization.Permission.Admin);

See SecurityProtectedWebApiEndpoint in sample project

Configuration

This section details how to configure Futurum.WebApiEndpoint.Micro to make the most out of its features for specific use cases. Apart from configuring the entire API, it will also show you how to configure a specific API version, and individual WebApiEndpoint(s).

Configuring Futurum.WebApiEndpoint.Micro

This allows you to configure:

  • DefaultApiVersion (mandatory)
    • This is used if a ApiVersion is not provided for a specific WebApiEndpoint.
  • OpenApi
    • DefaultInfo
      • This is used if a OpenApiInfo is not provided for a specific ApiVersion
    • VersionedOverrideInfo (optional)
      • Allows you to have an OpenApiInfo per a specific ApiVersion.
        • If you do not provide an OpenApiInfo for a specific ApiVersion, then the DefaultInfo is used.
        • If you do provide an OpenApiInfo for a specific ApiVersion and provide a property, then that versioned property is used.
        • If you do provide an OpenApiInfo for a specific ApiVersion, but don't provide a property, then the DefaultInfo property is used.
        • NOTE: Extensions is a dictionary, an Extensions for a specific ApiVersion is merged with the default Extensions, with the specific ApiVersion being used as the override (by key) if necessary.
  • Version
    • Prefix (optional)
    • Format (optional)
      • uses 'Asp.Versioning.ApiVersionFormatProvider'

Example in program.cs

builder.Services
       .AddWebApiEndpoints(new WebApiEndpointConfiguration
       {
           DefaultApiVersion = WebApiEndpointVersions.V1_0.Version,
           OpenApi = new()
           {
               DefaultInfo = new()
               {
                   Title = "Futurum.WebApiEndpoint.Micro.Sample",
               },
               VersionedOverrideInfo =
               {
                   {
                       WebApiEndpointVersions.V3_0.Version,
                       new WebApiEndpointOpenApiInfo
                       {
                           Title = "Futurum.WebApiEndpoint.Micro.Sample v3"
                       }
                   }
               }
           }
       });

See program.cs in sample project

The configuration is applied in the following order:

flowchart TB
   classDef blackText stroke:#000,color:#000;
   entire-api[1. Entire API] --> specific-version-api[2. Specific Version API]
   specific-version-api --> endpoint-configure[3. WebApiEndpoint 'Configure' Method]
   endpoint-configure --> endpoint-build[4. WebApiEndpoint 'Build' Method]
   endpoint-build --> minimal-api[5. Individual Minimal API]

   style entire-api fill:#f9d0c4
   style specific-version-api fill:#fcbf49
   style endpoint-configure fill:#90be6d
   style endpoint-build fill:#43aa8b
   style minimal-api fill:#577590

   class entire-api,specific-version-api,endpoint-configure,endpoint-build,minimal-api blackText

1. Configuring the entire API

The entire API can be configured to set global parameters. This is an ideal place to set configurations such as:

  • Global route prefix: This is a common prefix for all routes in your API.
  • Global authorization: This is where you can set the authorization required for all endpoints in your API. Remember to use the AllowAnonymous attribute on individual WebApiEndpoints that should not be secured, such as a Login endpoint.

To configure the entire API, you need to create a class that implements the IGlobalWebApiEndpoint interface.

Note: There can only be one class that implements IGlobalWebApiEndpoint. This is enforced by an analyzer. Analyzers work per project, so if you have GlobalWebApiEndpoint in more than 1 project, only the first will be registered. The order is dictated by the order of the AddWebApiEndpointsFor calls. See here

Note: The configuration set in this class is applied before the version route is created.

Here is an example of how to implement this:

Example

public class GlobalWebApiEndpoint : IGlobalWebApiEndpoint
{
    public IEndpointRouteBuilder Configure(IEndpointRouteBuilder builder, WebApiEndpointConfiguration configuration)
    {
        // Set the global route prefix to "api" and require admin authorization for all endpoints
        return builder.MapGroup("api").RequireAuthorization(Authorization.Permission.Admin);
    }
}

See GlobalWebApiEndpoint in sample project

2. Configuring a specific API version

In the context of API development, it's often necessary to configure specific API versions. This configuration can include aspects such as:

  • Authorization specific to the API version: This is where you can set the authorization required for all endpoints in a specific API version. Don't forget to use the AllowAnonymous attribute on individual WebApiEndpoints that should not be secured, such as a Login endpoint.

To configure a specific API version, you need to create a class that:

  • Implements the IWebApiVersionEndpoint interface.
  • Is decorated with at least one WebApiVersionEndpointVersion attribute, indicating the version(s) it applies to.

Note: There can only be one class that configures a specific API version. This is enforced by a Roslyn analyzer. Analyzers work per project, so if you have the same WebApiVersionEndpoint for a version in more than 1 project, only the first will be registered. The order is dictated by the order of the AddWebApiEndpointsFor calls. See here

Note: The configuration set in this class is applied after the version route is created, but before the specific WebApiEndpoint route is created.

Here's an example of how to implement this:

Example

[WebApiVersionEndpointVersion(WebApiEndpointVersions.V3_0.Number)]
[WebApiVersionEndpointVersion(WebApiEndpointVersions.V1_20_Beta.Text)]
public class WebApiVersionEndpoint3_0a : IWebApiVersionEndpoint
{
    public RouteGroupBuilder Configure(IEndpointRouteBuilder builder, WebApiEndpointConfiguration configuration)
    {
        // Set the route group to "test-api" and require admin authorization for all endpoints
        return builder.MapGroup("test-api").RequireAuthorization(Authorization.Permission.Admin);
    }
}

See WebApiVersionEndpoint3_0a in sample project

Remember, the configuration of specific API versions is a crucial aspect of maintaining and managing your APIs, especially when dealing with different versions of the same API. It allows you to control the behavior of each version independently, providing flexibility and control over your API's functionality.

3. WebApiEndpoint 'Configure' Method

See here

4. WebApiEndpoint 'Build' Method

See here.

5. Individual Minimal API

See here.

Sandbox runner

The Sandbox Runner is a feature that provides a set of comprehensive extension methods to execute your code in a controlled environment or "sandbox".

Run and RunAsync - Handling code that returns an IResult

These extension methods are designed to handle code that returns an IResult. The behavior of these methods is as follows:

  • If your code executes without throwing an unhandled exception, the original return value remains unchanged.
  • If your code throws an unhandled exception, a BadRequest<ProblemDetails> is returned, containing relevant details about the exception.

The returned Results<...> type is always expanded to include BadRequest<ProblemDetails>.

TIResult1 -> Results<TIResult1, 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>>

The Results type can accommodate a maximum of 6 types. Therefore, up to 5 types are allowed, reserving one space for BadRequest<ProblemDetails>.

Example use

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");
    }
}

In this example the Execute method is wrapped by the runner. It returns:

  • a NotFound if the file does not exist
  • a FileStreamHttpResult if the file exists
Results<NotFound, FileStreamHttpResult>

The Run / RunAsync extension method modifies this to include BadRequest<ProblemDetails>.

Results<NotFound, FileStreamHttpResult, BadRequest<ProblemDetails>>

Note: It is recommended to add the following to your GlobalUsings.cs file.

global using static Futurum.WebApiEndpoint.Micro.WebApiEndpointRunner;

This allows you to use the helper functions without having to specify the namespace, as demonstrated in the examples.

RunToOk and RunToOkAsync - If your code returns void or T (not a IResult)

These extension methods are designed to handle code that returns void or T. The behavior of these methods is as follows:

  • If your code executes without throwing an unhandled exception, the original return value remains unchanged.
  • If your code throws an unhandled exception, a BadRequest<ProblemDetails> is returned, containing relevant details about the exception.

The returned Results<...> type is always expanded to include BadRequest<ProblemDetails>.

void -> Results<Ok, BadRequest<ProblemDetails>>

T -> Results<Ok<T>, BadRequest<ProblemDetails>>

Example use

private static Results<Ok<IAsyncEnumerable<Todo>>, BadRequest<ProblemDetails>> GetAllHandler(HttpContext context, SqliteConnection db)
{
    return RunToOk(Execute, context, "Failed to get todos");

    IAsyncEnumerable<Todo> Execute() =>
        db.QueryAsync<Todo>("SELECT * FROM Todos");
}

In this example the Execute method returns IAsyncEnumerable<Todo>

IAsyncEnumerable<Todo>

The RunToOk / RunToOkAsync extension method will

  • change the T to Ok<T>
  • add BadRequest<ProblemDetails>.
Results<Ok<IAsyncEnumerable<Todo>>, BadRequest<ProblemDetails>>

Note: It is recommended to add the following to your GlobalUsings.cs file.

global using static Futurum.WebApiEndpoint.Micro.WebApiEndpointRunner;

This allows you to use the helper functions without having to specify the namespace, as demonstrated in the examples.

Exception Handler behavior customization

Implement and register in DI you're own IWebApiEndpointRunnerExceptionHandlerService.

Uploading file(s) with additional JSON payload

This section guides you on how to upload files with additional JSON payload using Futurum.WebApiEndpoint.Micro.

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 RunAsync(Execute, context, ToOk, "Failed to read file");

    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 RunAsync(Execute, context, ToOk, "Failed to read file");

    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;
    }
}

Additional helper functions

The final section provides a detailed overview of the additional helper functions that aid in program development.

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")

Comprehensive samples

There are examples showing the following:

Comprehensive samples

Security example

How to use in Swagger UI:

  1. Run the Sample project
  2. In the Swagger UI, go to the 'Security' 'Login' endpoint
  3. Set the following
    • Username = user1
    • Password = password1
    • SetPermissions = true
    • SetClaim = true
    • SetRole = true
  4. Copy the value returned without double quotes.
  5. Go to the 'Security' 'Protected' endpoint
  6. Click on the padlock
  7. In the value textbox, enter "Bearer " (don't forget the space at the end) + the value returned from the 'Login' endpoint that you copied in step 4.
  8. Click "Authorize"
  9. Run the 'Protected' endpoint

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();

Extendable GlobalExceptionHandler

Built in support for handling unhandled exceptions, returning a ProblemDetails response.

You can extend the GlobalExceptionHandler by adding your own custom exception handling and overriding the default exception handler.

NOTE: ExceptionToProblemDetailsMapperService is not thread-safe for either:

  • adding custom exception to ProblemDetails mapping
  • overriding default exception to ProblemDetails mapping

It is recommended to do this in the program.cs file.

Add to program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

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

app.UseExceptionHandler();

See program.cs in sample project

Add custom Exception to ProblemDetails mapping

In program.cs add the following:

ExceptionToProblemDetailsMapperService.Add<CustomException>((exception, httpContext, errorMessage) => new()
{
    Detail = "An custom error occurred.",
    Instance = httpContext.Request.Path,
    Status = StatusCodes.Status500InternalServerError,
    Title = ReasonPhrases.GetReasonPhrase(StatusCodes.Status500InternalServerError)
});

Override the Default Exception to ProblemDetails mapping

In program.cs add the following:

ExceptionToProblemDetailsMapperService.OverrideDefault((exception, httpContext, errorMessage) => new()
{
    Detail = "An error occurred.",
    Instance = httpContext.Request.Path,
    Status = StatusCodes.Status500InternalServerError,
    Title = ReasonPhrases.GetReasonPhrase(StatusCodes.Status500InternalServerError)
});

Tips & Tricks

How to organise your Api Versions

You want to avoid duplicating the Api Versions in multiple places. So it's recommended to create a class that contains all the Api Versions.

public static class WebApiEndpointVersions
{
    public static class V1_0
    {
        public const double Number = 1.0d;
        public static readonly WebApiEndpointVersion Version = WebApiEndpointVersion.Create(Number);
    }
    
    public static class V4_0_Alpha
    {
        public const double Number = 4.0d;
        public const string Status = "alpha";
        public static readonly WebApiEndpointVersion Version = WebApiEndpointVersion.Create(Number, Status);
    }

    public static class V1_20_Beta
    {
        public const string Text = "1.20-beta";
        public static readonly WebApiEndpointVersion Version = WebApiEndpointVersion.Create(Text);
    }
}

See WebApiEndpointVersions in sample project

You can use this in your program.cs file like this.

builder.Services.AddWebApiEndpoints(new WebApiEndpointConfiguration(WebApiEndpointVersions.V1_0.Version)

See program.cs in sample project

You can use this in your WebApiEndpoint like this.

[WebApiEndpoint("openapi")]
[WebApiEndpointVersion(WebApiEndpointVersions.V1_0.Number)]
public partial class OpenApiVersionV1WebApiEndpoint
{
    ...
}

See OpenApiVersionV1WebApiEndpoint.cs in sample project

FAQ (Frequently Asked Questions)

When to use WebApiEndpointRunner?

  • ✅ when you want the OpenApi specification to correctly reflect error handling via BadRequest<ProblemDetails>
  • ✅ when you want to set a custom error message

Our recommendation is to use WebApiEndpointRunner for all your WebApiEndpoints.

Troubleshooting

No operations defined in spec!

If you see this error in the SwaggerUI - No operations defined in spec! - then it means you haven't added any WebApiEndpoint projects. You need to do this for each project, including the project that contains the program.cs file. See this section for more details.

If there are Rest Api's that are not being picked up

If there are Rest Api's that are not being picked up, then it means you haven't added a WebApiEndpoint projects. You need to do this for each project, including the project that contains the program.cs file. See this section for more details.

Roslyn Analysers

FWAEM0001 - Non empty constructor found on WebApiEndpoint

We recommend that WebApiEndpoint's have an empty constructor and to take any injectable dependencies as parameters via the minimal API method itself.

Constructor dependencies will have a lifetime outside of the minimal API lifetime and could have unintended consequences.

FWAEM0002 - BadRequest without 'ProblemDetails' use found on WebApiEndpoint

Minimal API methods returning a 'BadRequest', should ensure that the 'BadRequest' is created with a 'ProblemDetails' instance.

FWAEM0003 - Multiple instances found of GlobalWebApiEndpoint

Checks to ensure that there is only one instance of GlobalWebApiEndpoint.

Analyzers work per project, so if you have GlobalWebApiEndpoint in more than 1 project, only the first will be registered. The order is dictated by the order of the AddWebApiEndpointsFor calls.

FWAEM0004 - Multiple instances found of WebApiVersionEndpoint for the same version

Checks to ensure that there is only one instance of WebApiVersionEndpoint.

Analyzers work per project, so if you have the same WebApiVersionEndpoint for a version in more than 1 project, only the first will be registered. The order is dictated by the order of the AddWebApiEndpointsFor calls.