Code Monkey home page Code Monkey logo

apiendpoints's Introduction

dotnet core - build Nuget Nuget

Follow @ardalis   Follow @nimblepros

ASP.NET Core API Endpoints

A project for supporting API Endpoints in ASP.NET Core web applications.

Sponsors

A HUGE Thank-You to AWS for sponsoring this project in June 2023 with an annual sponsorship!

Give a Star! ⭐

If you like or are using this project to learn or start your solution, please give it a star. Thanks!

Upgrade to 4.x Notes

The fluent generics and base types involved in ApiEndpoints were updated in version 4.x, resulting in breaking changes. The updates required should be pretty straightforward, and have a few additional features that weren't supported in previous versions.

The two main changes introduced in v4 are:

  • Base classes should now use EndpointBaseSync or EndpointBaseAsync
  • WithResponse has been modified to WithResult or WithActionResult

The result of an endpoint corresponds to the return type from the Handle method. Since ASP.NET Core MVC refers to these as some variation of ActionResult, that's the term we are using in this package now as well. The Response your endpoint may return refers to any data/DTO that is being sent to the client as part of the Result. If you wish to preserve your existing v3 functionality that specified WithResponse<T> you should be able to replace all such occurrences with WithActionResult<T>. However, if you need to specify a different kind of Result, such as a FileResult, you can now use something like WithResult<FileResult> to achieve this.

An endpoint that previously inherited from the synchronous BaseEndpoint should now inherit from EndpointBaseSync. Additionally, the WithResponse option now has optional non-generic versions, but if you were intending to return an ActionResult<T> you would now use WithActionResult<T> in your class definition, like so:

- public class ForecastEndpoint : BaseEndpoint
-     .WithRequest<ForecastRequestDto>
-     .WithResponse<IEnumerable<WeatherForecast>>
+ public class ForecastEndpoint : EndpointBaseSync
+     .WithRequest<ForecastRequestDto>
+     .WithActionResult<IEnumerable<WeatherForecast>>

The above change typically would not require any change to the Handle method. Endpoints that inherited from BaseAsyncEndpoint would now use EndpointBaseAsync. You can also just inherit from EndpointBase directly (without the .With* additions) which will provide you with a controller with a single Handle method without restrictions on parameter amount and type, if you need more flexibility than the fluent generic interface provides.

Upgrade to 3.x Notes

For version 3.0 we implemented a new way to define the base classes using "fluent generics". You can watch a video of what you need to know to apply them to your site here.

Table of Contents

1. Motivation

2. Introducing ASP.NET Core API Endpoints

3. Getting Started

4. Animated Screenshots

5. Open Questions

6. Roadmap

7. Related Articles

8. Videos and Podcasts

9. Related / Similar Projects

10. Projects Using ApiEndpoints

11. Success Stories and Testimonials

1. Motivation

MVC Controllers are essentially an antipattern. They're dinosaurs. They are collections of methods that never call one another and rarely operate on the same state. They're not cohesive. They tend to become bloated and to grow out of control. Their private methods, if any, are usually only called by a single public method. Most developers recognize that controllers should be as small as possible (unscientific poll), but they're the only solution offered out of the box, so that's the tool 99% of ASP.NET Core developers use.

You can use tools like MediatR to mitigate the problem. You can read a detailed article about how to migrate from Controllers to Endpoints using MediatR. The short version is that MediatR enables you to have single-line action methods that route commands to handlers. This is objectively a better approach, resulting in more cohesive classes that better follow OO principles. But what if you didn't even need that extra plumbing?

That's what ASP.NET Core API Endpoints are all about.

Side note: Razor Pages

The .NET team already did this exact thing with razor pages. They recognized that dealing with Views, ViewModels, Controllers, and Actions was way more complicated than necessary. It required a developer to jump around between at least 3 (and often more) different folders in order to add or modify a new page/view to their project. Razor pages addressed this by rethinking the model for page-based ASP.NET Core MVC endpoints.

Razor Pages group each page's razor markup, its related action(s), and its model into two linked files. It uses the same MVC features as the rest of the platform, so you still get routing, model binding, model validation, filters, the works. You literally give up nothing. But now when you need to add or modify a page you need to look at exactly 2 files, which are linked in the IDE so you don't need to scroll around the file system looking for them.

2. Introducing ASP.NET Core API Endpoints

ASP.NET Core API Endpoints are essentially Razor Pages for APIs. They break apart bloated controllers and group the API models used by individual endpoints with the endpoint logic itself. They provide a simple way to have a single file for the logic and linked files for the model types.

When working with ASP.NET Core API Endpoints your project won't need any Controller classes. You can organize the Endpoints however you want. By feature. In a giant Endpoints folder. It doesn't matter - they'll work regardless of where you put them.

Most REST APIs have groups of endpoints for a given resource. In Controller-based projects you would have a controller per resource. When using API Endpoints you can simply create a folder per resource, just as you would use folders to group related pages in Razor Pages.

Instead of Model-View-Controller (MVC) the pattern becomes Request-EndPoint-Response(REPR). The REPR (reaper) pattern is much simpler and groups everything that has to do with a particular API endpoint together. It follows SOLID principles, in particular SRP and OCP. It also has all the benefits of feature folders and better follows the Common Closure Principle by grouping together things that change together.

3. Getting Started

I'll look to add detailed documentation in the future but for now here's all you need to get started (you can also check the sample project):

  1. Add the Ardalis.ApiEndpoints NuGet package to your ASP.NET Core web project.
  2. Create Endpoint classes by inheriting from either EndpointBaseSync<TRequest,TResponse> (for endpoints that accept a model as input) or EndpointBaseSync<TResponse> (for endpoints that simply return a response). For example, a POST endpoint that creates a resource and then returns the newly created record would use the version that includes both a Request and a Response. A GET endpoint that just returns a list of records and doesn't accept any arguments would use the second version.
  3. Implement the base class's abstract Handle() method.
  4. Make sure to add a [HttpGet] or similar attribute to your Handle() method, specifying its route.
  5. Define your TResponse type in a file in the same folder as its corresponding endpoint (or in the same file if you prefer).
  6. Define your TRequest type (if any) just like the TResponse class.
  7. Test your ASP.NET Core API Endpoint. If you're using Swagger/OpenAPI it should just work with it automatically.

Adding common endpoint groupings using Swagger

In a standard Web API controller, methods in the same class are grouped together in the Swagger UI. To add this same functionality for endpoints:

  1. Install the Swashbuckle.AspNetCore.Annotations
dotnet add package Swashbuckle.AspNetCore.Annotations
  1. Add EnableAnnotations to the Swagger configuration in Startup.cs
services.AddSwaggerGen(c => {
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
    c.EnableAnnotations();
});
  1. Add the following attribute to endpoint methods
[HttpPost("/authors")]
[SwaggerOperation(
    Summary = "Creates a new Author",
    Description = "Creates a new Author",
    OperationId = "Author_Create",
    Tags = new[] { "AuthorEndpoint" })
]
public override async Task<ActionResult<CreateAuthorResult>> HandleAsync([FromBody]CreateAuthorCommand request)
{
    var author = new Author();
    _mapper.Map(request, author);
    await _repository.AddAsync(author);

    var result = _mapper.Map<CreateAuthorResult>(author);
    return Ok(result);
}

Option to use service dependency injection instead of constructor

// File: sample/SampleEndpointApp/Endpoints/Authors/List.cs
public class List : BaseAsyncEndpoint
    .WithRequest<AuthorListRequest>
    .WithResponse<IList<AuthorListResult>>
{
    private readonly IAsyncRepository<Author> repository;
    private readonly IMapper mapper;

    public List(
        IAsyncRepository<Author> repository,
        IMapper mapper)
    {
        this.repository = repository;
        this.mapper = mapper;
    }
    [HttpGet("/authors")]
    [SwaggerOperation(
        Summary = "List all Authors",
        Description = "List all Authors",
        OperationId = "Author_List",
        Tags = new[] { "AuthorEndpoint" })
    ]
    public override async Task<ActionResult<IList<AuthorListResult>>> HandleAsync(

        [FromQuery] AuthorListRequest request,
        CancellationToken cancellationToken = default)
    {
        if (request.PerPage == 0)
        {
            request.PerPage = 10;
        }
        if (request.Page == 0)
        {
            request.Page = 1;
        }
        var result = (await repository.ListAllAsync(request.PerPage, request.Page, cancellationToken))
            .Select(i => mapper.Map<AuthorListResult>(i));

        return Ok(result);
    }
}

Examples of the configuration can be found in the sample API project

File Upload Example

See Issue 170 for more details

using Ardalis.ApiEndpoints;
using Microsoft.AspNetCore.Mvc;

namespace SampleEndpointApp.Endpoints.Authors;

public class File : EndpointBaseAsync
    .WithRequest<IFormFile>
    .WithResult<ActionResult<string[]>>
{
  /// <summary>
  /// Post author's photo or something
  /// </summary>
  [HttpPost("api/[namespace]/file")]
  public override async Task<ActionResult<string[]>> HandleAsync(
    IFormFile file,
    CancellationToken cancellationToken = default)
  {
    string filePath = Path.GetTempFileName();
    using (var fileStream = System.IO.File.Create(filePath))
    {
      await file.CopyToAsync(fileStream, cancellationToken);
    }

    return new[]
    {
      filePath,
      file.FileName,
      file.ContentType,
      file.ContentDisposition,
      file.Length.ToString()
    };
  }
}

4. Animated Screenshots

Working with Endpoints, Requests, and Results in Visual Studio

api-endpoints-2

5. Open Questions

Below are what I expect will be some common questions:

How do I use shared routing conventions?

If you want to create a common route template for all or some subset of your Endpoints, simply create a EndpointBaseSync of your own that inherits from Ardalis.Api.Endpoints.EndpointBaseSync and add a [Route] attribute to it.

After refactoring to use the fluent generics pattern, there is no longer a way to use a base class for a default route. Instead, you should define your routes as constants which you can store in a central file or in each Request DTO (the sample shows this approach).

Can I add more than one public routable method to an Endpoint class?

Technically, yes. But don't do that. If you really want that, you should just use a Controller.

How can I bind parameters from multiple locations to my model?

To do this, you'll need to decorate the properties of your model with the proper route attributes:

public class NewArticleRequest
{
    [FromRoute(Name = "username")] public string Username { get; set; }
    [FromRoute(Name ="category")] public string Category { get; set; }

    [FromBody] public Article Article { get; set; }
}

Then, it's very important to include [FromRoute] in the method declaration in your endpoint using that model:

public override Task<ActionResult> HandleAsync([FromRoute] NewArticleRequest request)

Note the [Route("/article")] and [HttpPost("{username}/{category}")] lines below. These lines form the route string used in the NewArticleRequest class above.

[Route("/article")]
public class Post : BaseAsyncEndpoint
    .WithRequest<NewArticleRequest>
    .WithoutResponse
{
    [HttpPost("{username}/{category}")]
    [SwaggerOperation(
        Summary = "Submit a new article",
        Description = "Enables the submission of new articles",
        OperationId = "Article_Create",
        Tags = new[] {"Article"})
    ]
    public override Task<ActionResult> HandleAsync([FromRoute] NewArticleRequest request,
        CancellationToken cancellationToken = new CancellationToken())
    {
        //// your implementation
    }
}

For more information, take a look at this discussion and this issue. Thank you to @garywoodfine and @matt-lethargic.

How can I return a File result from an ApiEndpoint?

There's an example in the sample app that shows how to set this up and return a File actionresult. For the base type, just use the WithoutResponse option and in the endpoint handler return File().

How can I use model binding to pull values from multiple places, like [FromRoute], [FromBody], etc.?

#172 The base endpoints only expose a single model type which is used on the Handle method, so you can't easily add additional parameters to the Handle method. However, you can put as many properties on the associated Request DTO as you want, and model binding allows you to set the same attributes per property as you would have set per parameter on the action method. See Model Binding Docs and discussion here in issue 42

How can I use streaming from server to client?

There's an example in the sample app that shows how to set this up and return an IAsyncEnumerable<T>. For the base type, just use the WithAsyncEnumerableResult<T> option and in the endpoint handler yeld return after awaiting your async code.

Note: streaming with IAsyncEnumerable does not work within Swagger Ui. Use curl to test this functionality

curl -X "GET" "https://localhost:44338/api/Authors/stream" -H "accept: text/plain" 

6. Roadmap

The following are some things I'd like to add to the project/package.

Item Template

Visual Studio and/or CLI item templates would make it much easier to create Endpoints and their associated models, with the correct naming so they're linked in the IDE.

Route Conventions

One thing that Controllers do have is built-in support in the framework to use their name in routes (e.g. "[controller]/{id?}"). Currently in the sample app routes are hard-coded strings. It would be nice if there were an easy way to use a convention based on foldername or namespace or something (using foldername would align with how Razor Pages routing works).

7. Related Articles

8. Videos and Podcasts

9. Related / Similar Projects

10. Projects Using ApiEndpoints

If you're using them or find one not in this list, feel free to add it here via a pull request!

  • CleanArchitecture: A solution template for ASP.NET 3.x solutions using Clean Architecture.
  • PayrollProcessor: A smorgasbord of modern .NET tech written with functional and asynchronous patterns.
  • eShopOnWeb: Sample ASP.NET Core reference application, powered by Microsoft

11. Success Stories and Testimonials

"I have implemented in my team your API endpoint solution and I must tell you that was a pretty good investment! in particular how maintainable and testable the solution became!"

Nuno Santos

apiendpoints's People

Contributors

andrew-hillier avatar ardalis avatar azaferany avatar cmxl avatar davidhenley avatar dependabot-preview[bot] avatar dependabot[bot] avatar hawxy avatar henriksen avatar idormenco avatar ilyanadev avatar jasdefer avatar jeastham1993 avatar joeymckenzie avatar kylemcmaster avatar maxkoshevoi avatar mikesigs avatar paulvanbladel avatar ppittle avatar rafaelbm avatar reapism avatar scottsauber avatar sdepouw avatar shojy avatar smithgeek avatar swizkon avatar szlatkow avatar tbalasavage avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

apiendpoints's Issues

Question - Would it make sense to Unify BaseAsyncEndpoint and BaseEndpoint?

I was curious if having a singular root (Base)Endpoint class with the determining of whether the Endpoint was Async or Sync left till the end would make sense.

Not sure if something like the below would make any difference with developer experience or usage.

public static class Endpoint
{
 ... // As is now with the current request classes
    public abstract WithAsyncResponse<TResp> : BaseEndpoint
    {
         // Async method
    }

    public abstract WithResponse<TResp> : BaseEndpoint
    {
         // Sync method
    }
// Without options
...
}

Add Screenshots and Walkthrough

Need a more detailed walkthrough with screenshots showing linked files, etc. as well as demonstrating how the endpoints work in Swagger or Postman

Swagger 'Fetch Error' and optional parameters via query string

  1. If you have an endpoint with an [HttpGet( "/calc-engines/{name}" )] attribute but you want to accept optional query string parameters (i.e. bool? allVersions), how do you declare your BaseAsyncEndpoint?

I have public class Get : BaseAsyncEndpoint<Get.Query, Get.CalcEngine[]> and then have a Query object with name and allVersions properties, but when I hit swagger, it shows this:

image

New to creating web apis, is the 'optional' values on the querystring bad practice? Note that I have a [HttpGet( "/calc-engines" )] route that returns all information versus detailed information for one item.

  1. Swagger issue (and I can't seem to find any logging saying why it is failing). When I only had one endpoint, everything worked great. But when I added two endpoints, I received:

Fetch error
undefined /swagger/v1/swagger.json

The following shows my endpoint code. Both are essentially the same and I haven't decided if I'm keeping MediatR or not, but I haven't yanked out yet so just pasting. Note that if I remove one or the other, Swagger works, only fails when both are present.

public class Get : BaseAsyncEndpoint<List.Query, List.CalcEngine[]>
{
	private readonly IMediator mediator;

	public Get( IMediator mediator ) => this.mediator = mediator;

	[HttpGet( "/calc-engines/{name}" )]
	[SwaggerOperation(
		Summary = "Get detailed information about a CalcEngine",
		Description = "Get Live and Test CalcEngine information and all prior version information for each",
		OperationId = "CalcEngines.Get",
		Tags = new[] { "CalcEngines" } )
	]
	public async override Task<ActionResult<CalcEngine[]>> HandleAsync(
		[FromQuery] query,
		CancellationToken cancellationToken = default ) => await mediator.Send( query, cancellationToken );

	public record Query : IRequest<CalcEngine[]>
	{
		public string name { get; init; }
		public bool allVersions { get; init; } = false;
	}

	public record CalcEngine
	{
		public string Name { get; init; }
		...
	}

	class Handler : IRequestHandler<Query, CalcEngine[]>
	{
		public Task<CalcEngine[]> Handle( Query message, CancellationToken token )
		{
			var results = new[]
			{
				new CalcEngine ...
				new CalcEngine ...
			};

			return Task.FromResult( results );
		}
	}
}

Thanks in advance.

Suggestion: using fluent generics

Inspired by this article: https://tyrrrz.me/blog/fluent-generics
I tried it here:
see: https://github.com/paulvanbladel/ApiEndpoints

It's about making the endpoint signature more expressive by using :

 public class Create : Endpoint.WithRequest<CreateAuthorCommand>.WithResponse<CreateAuthorResult>
    {
        private readonly IAsyncRepository<Author> _repository;
        private readonly IMapper _mapper;

        public Create(IAsyncRepository<Author> repository,
            IMapper mapper)
        {
            _repository = repository;
            _mapper = mapper;
        }

        [HttpPost("/authors")]
        [SwaggerOperation(
            Summary = "Creates a new Author",
            Description = "Creates a new Author",
            OperationId = "Author.Create",
            Tags = new[] { "AuthorEndpoint" })
        ]

Migrating to the controller-less pattern

This sentence: The .NET team already did this exact thing with razor pages. really brought home for me the bigger picture. I'm ready to buy in 100%. If the old way was controllers mainly for sake of routes (opened the door for bloated controllers with too many dependencies) - then this 'razor-pages-for-apis' approach is the basically replacing the conventional controller with a CQRS pattern, isn't it?

And ApiEndpoints gives us linked files which overcomes what used to be very awkward -- navigated between where the event was raised and where the handler's code sat, yes?

How exactly are these files being linked so they show in VS as they do?

Support routable attributes on records

Support routable attributes on records. Currently the [FromHeader], [FromQuery] attributes only work when the request is a class but not with records.

Make Ardalis.ApiEndpoints.CodeAnalyzers less coupled with ApiEndpoints project

I would like to use your approach with one single action per controller but don't want to inherit from BaseEndpoint as it will be tricky to update all current methods for Handle usage.

Instead I would like to create my own empty Endpoint base class and reuse your CodeAnalyzer with autofix behaviour to create separate endpoint files per action.

Best/Cleanest way of handling different versions?

Greetings and salutations.

Absolutely love the idea of this and I am very keen to start using it in future projects. My only question is around versioning in a longer term project. What would be the cleanest way of doing this? Would it be best to just add the versioned operations on each endpoint file, or would it be better to create a new file per version or is there another way entirely?

I don't mind if the suggested way is opinionated (I would probably prefer it - as long as it makes sense from a long term maintenance perspective).

Hopefully my question isn't too open ended or subjective - I apologize if it is.

Thanks!

Question - is it possible to bind both FromQuery and Body object using this library

It is possible to do this as I can't workout how based on the TRequest, TResponse definition. The below will obviously not compile.

Thanks

public class MyEndpoint : BaseEndpoint<PayloadBody, string>
...
[HttpPost("resource/{externalTicketId}")]
[Produces("application/json")]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(string))]
public override ActionResult Handle([FromQuery] string externalTicketId, PayloadBody payload)
{
....

Adding another level of abstraction to keep the returned object consistent

First of all I have to say after making the switch to razor pages last year that this is what I have been looking for.

However, I find the return type interesting as you can return anything from a ActionResult<TResult> and not just TResult.

Have you considered adding another level of abstraction so the HandleAsync returns TResult or even some type of result monad, it would go somewhat to helping #13, but I'm not sure how the FromBody attribute would work.

If this strong typing is something you would entertain for inclusion I might see what i can do over the weekend.

What do you think?

How to use CreatedAt* action result helpers

None of the CreatedAt* methods nor the LinkGenerator are able to create urls under pattern (for me at least).

Are there any work arounds? My signature looks like this

[HttpGet( Routes.Files.Download )]
[SwaggerOperation(
	Summary = "Download file",
	Description = "Download file",
	OperationId = "Files." + nameof( Routes.Files.Download ),
	Tags = new[] { "Files" } )]
[SwaggerOrder( 2 )]
[ProducesResponseType( typeof( FileStreamResult ), StatusCodes.Status200OK )]
[ProducesResponseType( StatusCodes.Status304NotModified )]
[ProducesResponseType( typeof( ValidationProblemDetails ), StatusCodes.Status401Unauthorized )]
[ProducesResponseType( typeof( ValidationProblemDetails ), StatusCodes.Status404NotFound )]
public async Task<IActionResult> HandleAsync( [FromQuery] Parameters parameters ) => await DownloadFileAsync( parameters.Id, dbConnectionFactory );

And the class name is Download.

linkGenerator.GetPathByAction( nameof( Download.HandleAsync ), nameof( Download ), new { id = fileId } ); returns null and return CreatedAtAction( nameof( Download.HandleAsync ), nameof( Download ), new { Id = fileId } ); throws an exception.

System.InvalidOperationException: No route matches the supplied values.
at Microsoft.AspNetCore.Mvc.CreatedAtRouteResult.OnFormatting(ActionContext context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsyncCore(ActionContext context, ObjectResult result, Type objectType, Object value)
at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsync(ActionContext context, ObjectResult result)
at Microsoft.AspNetCore.Mvc.ObjectResult.ExecuteResultAsync(ActionContext context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|21_0(ResourceInvoker invoker, IActionResult result)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|29_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler.HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)

Add async support

BaseEndpoint should support Task based options because many people use async tasks for IO.

Change `WithResponce` method names and introduce new ones

Motivation

Current naming is misleading: #105 (comment)
And incomplete: #125 (WithOkResponce it's not ideal name for that method)

Asp.Net Core allows way more flexibility for action return types.

Proposal

Leave WithRequest methods the same, but rename WithResponse methods and create new ones to be mapped to following return types:

  • WithoutResult -> void or Task (not possible with current implementation)
  • WithResult<TResponse> -> TResponse or Task<TResponse> (not possible with current implementation)
  • WithActionResult -> ActionResult or Task<ActionResult> (named WithoutResult in current implementation)
  • WithActionResult<TResponse> -> ActionResult<TResponse> or Task<ActionResult<TResponse>> (named WithResponse<TResponse> in current implementation)

This would allow far more flexibility when creating endpoint (by bringing it's feature set closer to what's possible in regular Asp.Net Core), and resolve confusing name of WithoutResult.

Since this is a breaking change, it would require major version change.

Notes

  • In Asp.Net Core it's also possible to return IActionResult, but that's an artifact from Asp.Net Core 1, and it's functionality is fully covered by ActionResult and ActionResult<T>, so no need to bring support for it here.

Create a Project Template for .NET 6

Modify the ASP.NET Core Web API / webapi project template to use API Endpoints instead of controllers, etc.

Put the code in a folder in this repo.

Publish it to NuGet via GitHub actions when it changes.

Complex Request Model ApiController and QueryString params

I think you will have problems binding to Complex Models if from query params if you don't
SuppressInferBindingSourcesForParameters.

Once you have a test case you can confirm that and then the following should fix it :)

      aServiceCollection.Configure<ApiBehaviorOptions>
      (
        aApiBehaviorOptions =>
        {
          aApiBehaviorOptions.SuppressInferBindingSourcesForParameters = true;
        }
      );

Binding parameters from multiple locations not working in .NET 6?

  • NuGet Package Version: 3.1.0
  • .NET SDK Version: 6.0-RC-1

I am trying to bind parameters from multiple locations as specified in the documentation, but it does not seem to work.

Endpoint:

[HttpPut("api/projects/{projectId}")]
public override async Task<ActionResult<UpdateProjectResponse>> HandleAsync([FromRoute] UpdateProjectRequest request, CancellationToken cancellationToken = default)
{

}

Request:

public class UpdateProjectRequest
{
    [FromRoute(Name = "projectId")] public Guid ProjectId { get; set; } = default!;

    [FromBody] public string Code { get; set; } = default!;

    [FromBody] public string Description { get; set; } = default!;

    [FromBody] public string Number { get; set; } = default!;
}

This is the response I get:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-8204c6f7087a1e60fb4c1ab5ac75c778-de42a5cad4fe62d6-00",
  "errors": {
    "$": [
      "The JSON value could not be converted to System.String. Path: $ | LineNumber: 0 | BytePositionInLine: 1.",
      "The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true. Path: $ | LineNumber: 0 | BytePositionInLine: 0."
    ],
    "Code": [
      "The Code field is required.",
      "'Code' must not be empty."
    ],
    "Number": [
      "The Number field is required.",
      "'Number' must not be empty."
    ],
    "Description": [
      "The Description field is required.",
      "'Description' must not be empty."
    ]
  }
}

I am also using FluentValidation, but that should not matter because FluentValidation validates after model binding.

Could this be because something has changed in .NET 6 or am I just missing something?

Great! But ...

Hello, this is not really an issue, as much as a set of thoughts...

Thank you for sharing this remarkable piece of open source software! I do feel however that it would be better suited as a PR to Asp.NET core, maybe as an official Microsoft package. It feels like something that inherently fits in Microsoft's mindset around Razor pages.

Secondly I noticed you discuss MediatR. I do think those concepts are related and not related at the same time.
MediatR (IMHO) sits between UI and Application/Domain/... whatever you call it. This package sits between IIS/Kestrel and UI so to speak.

It is perfectly possible (and I would even prefer it) to use this library and still use MediatR to dispatch Commands/Queries.

Thoughts?

Authorization support

Hi,

I'm very interested in this design, but I have not found in documentation how to setup the [Authorize] decorator. Is this possible?
If I create for example a custom Authentication Handler, for execute a policy, i will work with this solution?

Congratulations for that amazing work.

Thanks.
Jose.

Add Custom Response for File Download Endpoints

Hi Ardalis, please consider adding a custom return type for File types
public abstract class WithResponseCustoms : BaseEndpointAsync { public abstract Task HandleAsync( TRequest request, CancellationToken cancellationToken = default ); }

from #77 (comment)

Render the absolute url for a given endpoint

NuGet Package Version: 3.1.0
.NET SDK Version: .NET Core 3.1

I am trying to render the absolute url for a given endpoint using IUrlHelper but I can't get this to work.
I am using NamespaceConventions for my endpoints.

Example endpoint:

```
/// <summary>
/// Customer List ApiEndpoint
/// </summary>
[Authorize]
[Route("api/v{version:apiVersion}/customers")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public class List : BaseAsyncEndpoint
    .WithoutRequest
    .WithResponse<IList<CustomerListResponse>>
{
    private readonly ICustomerService _customerService;
    private readonly IMapper _mapper;

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="customerService"></param>
    /// <param name="mapper"></param>
    public List(
        ICustomerService customerService,
        IMapper mapper)
    {
        _customerService = Guard.Against.Null(customerService, nameof(customerService));
        _mapper = Guard.Against.Null(mapper, nameof(mapper));
    }

    /// <summary>
    /// Get a list of customers
    /// </summary>
    /// <param name="cancellationToken"></param>
    /// <returns>A list of customer objects</returns>
    [HttpGet(Name = "GetCustomers")]
    [Authorize(Policy = "ApiCustomersRead")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public override async Task<ActionResult<IList<CustomerListResponse>>> 
        HandleAsync(CancellationToken cancellationToken = default)
    {
        var customers = await _customerService.GetAsync();

        return _mapper.Map<List<CustomerListResponse>>(customers);
    }
}
```

I need to get the url as:

/api/v1.0/customers

Any idea on how to do this?

When using controllers and without apiVersion I used IUrlHelper and passed controller name and action method:

url.RouteUrl(new { controller = apiControllerName, action = actionName });

Create an Analyzer to Ensure only One Public Method per Endpoint

Endpoints are actually just controllers, and if they had more public methods beyond the one defined in the class, they could still act as Actions and be routed to by the framework. Create an Analyzer to warn if someone is adding additional public methods to a class that inherits from an Endpoint base class.

Suggestion: Exposing Cancellation Tokens

Current State

Currently, if developers want to control operation cancellation through CancellationToken, they are a little stuck, since the HandleAsync() method doesn't have a CancellationToken parameter.

MVC auto injects these values if they are present.

Motivation

In an app that renders HTML for the browser, this might not be as important, but in client side JavaScript applications, it's not uncommon for for a user to interact with multiple parts of the UI in quick succession (like typing in a search box) that would cause previous XHR requests to be canceled since those responses would be discarded by the client anyway - it's only interested in the latest response.

ASP.NET Core will still handle the cancellation and not send the response, but the developer's pipeline of operations continue to execute on the previous request thread while the newer request is handled.

Many external service libraries (DB, HTTP) accept CancellationTokens to halt operations.

Solution

It would be nice to be able to support cancellation from end-to-end with ApiEndpoints.

I believe we'd need to change the HandleAsync() API to accept a CancellationToken token as the last parameter, which means a breaking change.

Thoughts

I'd be willing to PR this if you are interested in the idea.

I'm also wondering, since it would be a breaking change, if there were any other solutions to problems you'd like to roll in with it, like supporting multiple method params (not sure how... maybe multiple generics like the BCL does it with Func<> and Action<>?)

Services as plugin

Services in separate dll files through reflection? Just like plugins?
e.g. JWT Login
e.g. Role Manager

Attribute routes with the same name 'HandleAsync' must have the same template

  • NuGet Package Version: 3.1.0
  • .NET SDK Version: .NET Core 3.1

Steps to Reproduce:

  1. Create a new endpoint List
  2. Create a new endpoint Get

List.cs

/// <summary>
/// Customer List ApiEndpoint
/// </summary>
[Authorize]
[Route("api/v{version:apiVersion}/customers")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public class List : BaseAsyncEndpoint
    .WithoutRequest
    .WithResponse<IList<CustomerResponse>>
{
    private readonly ICustomerRepository _customerRepository;
    private readonly IMapper _mapper;

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="customerRepository"></param>
    /// <param name="mapper"></param>
    public List(ICustomerRepository customerRepository,
        IMapper mapper)
    {
        _customerRepository = customerRepository ?? throw new ArgumentNullException("ICustomerRepository can't be null.");
        _mapper = mapper ?? throw new ArgumentNullException("IMapper can't be null.");
    }

    /// <summary>
    /// Get a list of customers
    /// </summary>
    /// <param name="cancellationToken"></param>
    /// <returns>A list of customer objects</returns>
    [HttpGet(Name = nameof(List.HandleAsync))]
    [Authorize(Policy = "ApiCustomersRead")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public override async Task<ActionResult<IList<CustomerResponse>>> 
        HandleAsync(CancellationToken cancellationToken = default)
    {
        var result = await _customerRepository.GetAsync();

        return _mapper.Map<List<CustomerResponse>>(result);
    }
}

Get.cs

/// <summary>
/// Customer Get ApiEndpoint
/// </summary>
[Authorize]
[Route("api/v{version:apiVersion}/customers")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public class Get : BaseAsyncEndpoint
    .WithRequest<Guid>
    .WithResponse<CustomerDetailResponse>
{
    private readonly ICustomerRepository _customerRepository;
    private readonly IMapper _mapper;

    /// <summary>
    /// Constructor
    /// </summary>
    /// <param name="customerRepository"></param>
    /// <param name="mapper"></param>
    public Get(ICustomerRepository customerRepository,
        IMapper mapper)
    {
        _customerRepository = customerRepository ?? throw new ArgumentNullException("ICustomerRepository can't be null.");
        _mapper = mapper ?? throw new ArgumentNullException("IMapper can't be null.");
    }

    /// <summary>
    /// Get a customer
    /// </summary>
    /// <param name="customerId">The unique identifier of the customer</param>
    /// <param name="cancellationToken"></param>
    /// <returns>A customer object</returns>
    [HttpGet("{customerId:guid}", Name = nameof(Get.HandleAsync))]
    [Authorize(Policy = "ApiCustomersRead")]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status200OK)]
    public override async Task<ActionResult<CustomerDetailResponse>> 
        HandleAsync(Guid customerId, CancellationToken cancellationToken = default)
    {
        var result = await _customerRepository.GetByKeyAsync(customerId);

        if (result == null)
            throw new SkycountCustomerNotFoundException(customerId);

        return _mapper.Map<CustomerDetailResponse>(result);
    }
}

After running the API I got the following exception:

InvalidOperationException: The following errors occurred with attribute routing information:

Error 1:
Attribute routes with the same name 'HandleAsync' must have the same template:
Action: 'Skycount.Finance.Web.v1_0.Endpoints.Customers.Get.HandleAsync (Skycount.Finance.Web)' - Template: 'api/v{version:apiVersion}/customers/{customerId:guid}'
Action: 'Skycount.Finance.Web.v1_0.Endpoints.Customers.List.HandleAsync (Skycount.Finance.Web)' - Template: 'api/v{version:apiVersion}/customers'

Enable IActionResult Response

Research For Duplicates

I looked through the issues and docs and didn't see anything discussing this. Apologies if it's there and I missed it.

Describe the feature you'd like

So I love the idea of breaking my controller out like this and started updating my api template to use API Endpoints. Once I started though, I noticed that, as far as I can tell, I have to chose an explicit response for my endpoints that previously use IActionResult as described in the MS docs. This seems to be due to the WithResponse method wrapping whatever is passed to it in a Task<ActionResult<>>. This is obviously by design and I'm sure I can get things to at least working as is, but it seems like we should have the option here to do things, as far as I know, semantically correct.

Example

Here's an example for a POST.

    [Route("/api/patients")]
    public class PostPatientEndpoint : BaseAsyncEndpoint
        .WithRequest<PatientForCreationDto>
        .WithResponse<IActionResult>
    {
        private readonly IMediator _mediator;

        public PostPatientEndpoint(IMediator mediator)
        {
            _mediator = mediator;
        }

        /// <response code="201">Patient created.</response>
        /// <response code="400">Patient has missing/invalid values.</response>
        /// <response code="409">A record already exists with this primary key.</response>
        /// <response code="500">There was an error on the server while creating the Patient.</response>
        [ProducesResponseType(typeof(Response<PatientDto>), 201)]
        [ProducesResponseType(typeof(Response<>), 400)]
        [ProducesResponseType(typeof(Response<>), 409)]
        [ProducesResponseType(500)]
        [Consumes("application/json")]
        [Produces("application/json")]
        [SwaggerOperation(
            Summary = "Creates a new Patient record.",
            OperationId = "PostPatientEndpoint",
            Tags = new[] { "Patients" })
        ]
        [HttpPost, MapToApiVersion("1.0")]
        public override async Task<IActionResult> HandleAsync([FromBody] PatientForCreationDto patientForCreation,
            CancellationToken cancellationToken = new CancellationToken())
        {
            // add error handling
            var command = new AddPatient.AddPatientCommand(patientForCreation);
            var commandResponse = await _mediator.Send(command);
            var response = new Response<PatientDto>(commandResponse);

            return CreatedAtRoute("GetPatient",
                new { commandResponse.PatientId },
                response);
        }
    }

How to make decorator to work with endpoints?

@ardalis Is it possible to add decorator to Endpoints to enable extra actions in the endpoint execution pipeline? For example, I would like to log my endpoint request data. I don't wanna end up adding logging into each endpoint. Instead a decorator and apply the decorator to the selected endpoints so that it gets executed in the Endpoint Handle Pipeline?

AuditLogDecorator:

public class AuditLoggingDecorator<TRequest, TResponse> : BaseAsyncEndpoint<TRequest, TResponse>
{
    private readonly BaseAsyncEndpoint<TRequest, TResponse> _endpoint;

    public AuditLoggingDecorator(BaseAsyncEndpoint<TRequest, TResponse> endpoint)
    {
        _endpoint = endpoint;
    }

    public override async Task<ActionResult<TResponse>> HandleAsync(TRequest request,
        CancellationToken cancellationToken)
    {
        string requestJson = JsonSerializer.Serialize(request);

        // Use proper logging here
        Console.WriteLine($"Endpoint of type {request.GetType().Name}: {requestJson}");

        return await _endpoint.HandleAsync(request, cancellationToken);
    }
}

AuditLogAttribute:

[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public sealed class AuditLogAttribute : Attribute
{
    public AuditLogAttribute()
    {
    }
}

Endpoint:

[AuditLog]
public override async Task<ActionResult<ListCatalogBrandsResponse>> HandleAsync(CancellationToken cancellationToken)
{
...

With the above setup, HandleAsync inside AuditLogDecorator is not getting executed. Am I missing any hooks? Please assist.

Use Route and Body Parameters in an Endpoint

For a PUT request that is making an update to a resource, how would an API endpoint allow the ID of the resource to be specified on the ROUTE while the new state of the resource is specified in the request BODY?

Some example code that is not working:

public class Put : Endpoint.WithRequest<InvestmentTypePutRequest>.WithResponse<InvestmentTypePutResponse>
{
    [HttpPut("/investment-types/{Id:Guid}")]
    public override async Task<ActionResult<InvestmentTypePutResponse>> HandleAsync(InvestmentTypePutRequest request, CancellationToken cancellationToken = default)
     {
           // request.Id is null here
      }
}

public class InvestmentTypePutRequest
{
  [FromRoute] public Guid Id { get; set; }
  [FromBody] public byte[] Version { get; set; } = null!;
  [FromBody] public string Name { get; set; } = null!;
  [FromBody] public string? Description { get; set; }
}

shared routing conventions not possible with fluent generics API

I might be missing something here but it seems as if it's no longer possible to have any shared routing conventions with the new fluent generics API.

The current version of the docs say to create your own base class that inherits from BaseEndpoint, this is no longer possible since BaseEndpoint (and BaseEndpointAsync) are static classes which cannot be inherited from. Also, even if they could I believe the Route attribute has to be added to a class that inherits from ControllerBase which BaseEndpoint does not.

Add Related/Similar Projects to README

Add two sections to the README:

  1. Similar projects. A number of folks have pointed me at similar implementations and I'd like to link to them from here.

  2. Projects Using ApiEndpoints. I'd like to link to any projects I know of that are successfully using the ApiEndpoints package.

How to return FileResult using endpoints?

Dear Steve Smith

In controllers I used to return Files using return File(fileBytes, mime, "archive.zip"); and my controller return type will be IActionResult. How to achieve this with Endpoints? Please assist.

Thanks

Add security and versioning dependency alerts

  • Dependabot alerts on updates for the project's dated and vulnerable dependencies and automatically creates PRs to easily update them

Pipeline Foundation is a non-profit initiative with the sole purpose of giving back to the IT community by assisting OSS projects with DevOps implementations and best practices.

Built with ❤ by Pipeline Foundation.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.