Get Started
Follow the steps below to create your first endpoint that will handle an HTTP POST request and send a response back to the client.
Create Project & Install Package
dotnet new web -n MyWebApp
cd MyWebApp
dotnet add package FastEndpoints
Prepare Startup
Replace the contents of Program.cs file with the following:
using FastEndpoints;
var bld = WebApplication.CreateBuilder();
bld.Services.AddFastEndpoints();
var app = bld.Build();
app.UseFastEndpoints();
app.Run();
Add A Request DTO
Create a file called MyRequest.cs and add the following:
public class MyRequest
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
Add A Response DTO
Create a file called MyResponse.cs and add the following:
public class MyResponse
{
public string FullName { get; set; }
public bool IsOver18 { get; set; }
}
Add An Endpoint Class
Create a file called MyEndpoint.cs and add the following:
public class MyEndpoint : Endpoint<MyRequest, MyResponse>
{
public override void Configure()
{
Post("/api/user/create");
AllowAnonymous();
}
public override async Task HandleAsync(MyRequest req, CancellationToken ct)
{
await SendAsync(new()
{
FullName = req.FirstName + " " + req.LastName,
IsOver18 = req.Age > 18
});
}
}
Now run your app and send a POST request to the /api/user/create endpoint using a REST client such as Insomnia with the following request body:
{
"FirstName": "Marlon",
"LastName": "Brando",
"Age": 40
}
You should then get a response back such as this:
{
"FullName": "Marlon Brando",
"IsOver18": true
}
That's all there's to it.
You simply configure how the endpoint should be listening to incoming requests from clients in the Configure() section calling methods such as Get(), Post(), AllowAnonymous(), etc. Then you override the HandleAsync() method in order to specify your handling logic.
In this example, the request DTO is automatically populated from the JSON body of your HTTP request and passed in to the handler. After processing, the SendAsync() method is called with a new response DTO instance to be sent to the requesting client.
There's a bunch of assemblies the library excludes by default when scanning for endpoints for auto registration. If your endpoints happen to be located in one of those assembly names, you'll be greeted with an exception about FastEndpoints being unable to discover any endpoints. Quickest way to remedy that would be to rename your project to something that's not in the exclusion list, or manually specify an additional assembly to be scanned like so.
Endpoint Types
There are 4 different endpoint base types you can inherit from.
Endpoint<TRequest> - Use this type if there's only a request DTO. You can however send any object to the client that can be serialized as a response with this generic overload.
Endpoint<TRequest,TResponse> - Use this type if you have both request and response DTOs. The benefit of this generic overload is that you get strongly-typed access to properties of the DTO when doing integration testing and validations.
EndpointWithoutRequest - Use this type if there's no request nor response DTO. You can send any serializable object as a response here also.
EndpointWithoutRequest<TResponse> - Use this type if there's no request DTO but there is a response DTO.
It is also possible to define endpoints with EmptyRequest and EmptyResponse if needed like so:
public class MyEndpoint : Endpoint<EmptyRequest,EmptyResponse> { }
Fluent Generics
Alternatively, endpoint base classes can be selected for derivation using a fluent generic builder. Start with the Ep static entrypoint and choose request & response DTO types as needed.
// equivalent of Endpoint<TRequest>
public class MyEndpoint : Ep.Req<MyRequest>.NoRes { }
// equivalent of Endpoint<TRequest,TResponse>
public class MyEndpoint : Ep.Req<MyRequest>.Res<MyResponse> { }
// equivalent of EndpointWithoutRequest
public class MyEndpoint : Ep.NoReq.NoRes { }
// equivalent of EndpointWithoutRequest<TResponse>
public class MyEndpoint : Ep.NoReq.Res<MyResponse> { }
Sending Responses
There are multiple response sending methods you can use. It is also possible to simply populate the Response property of the endpoint and get a 200 OK response with the value of the Response property serialized in the body automatically. For ex:
public class MyResponse
{
public string FullName { get; set; }
public int Age { get; set; }
}
public class MyEndpoint : EndpointWithoutRequest<MyResponse>
{
public override void Configure()
{
Get("/api/person");
AllowAnonymous();
}
public override async Task HandleAsync(CancellationToken ct)
{
var person = await dbContext.GetFirstPersonAsync();
Response.FullName = person.FullName;
Response.Age = person.Age;
}
}
Assigning a new instance to the Response property has the same effect:
public override Task HandleAsync(CancellationToken ct)
{
Response = new()
{
FullName = "john doe",
Age = 124
};
return Task.CompletedTask;
}
Union-Type Returning Handler
Starting from .NET 7.0, Minimal APIs introduced the ability for endpoints to conditionally return one of multiple results/outcomes via a union type called Results<T1,T2,...>.
The result objects are instantiated by calling static methods on the TypedResults class depending on what kind of outcome is needed.
FastEndpoints also supports this strategy simply by setting the response DTO type of the endpoint's generic parameter to the desired union type and overriding the ExecuteAsync(...) handler method instead of the usual HandleAsync(...) method.
public class MyEndpoint : Endpoint<MyRequest,
Results<Ok<MyResponse>,
NotFound,
ProblemDetails>>
{
public override void Configure() { ... }
public override async Task<Results<Ok<MyResponse>, NotFound, ProblemDetails>> ExecuteAsync(
MyRequest req, CancellationToken ct)
{
await Task.CompletedTask; //simulate async work
if (req.Id == 0) //condition for a not found response
{
return TypedResults.NotFound();
}
if (req.Id == 1) //condition for a problem details response
{
AddError(r => r.Id, "value has to be greater than 1");
return new FastEndpoints.ProblemDetails(ValidationFailures);
}
// 200 ok response with a DTO
return TypedResults.Ok(new MyResponse
{
RequestedId = req.Id
});
}
}
If there's only one type of outcome, set the TResponse generic parameter of the endpoint to the desired IResult type like so:
public class MyEndpoint : EndpointWithoutRequest<NotFound>
{
public override void Configure() { ... }
public override async Task<NotFound> ExecuteAsync(CancellationToken ct)
{
await Task.CompletedTask;
return TypedResults.NotFound();
}
}
It is also possible to send TypedResults when using HandleAsync() as shown below with the use of SendResultAsync() method.
public class MyEndpoint : Endpoint<MyRequest, Results<Ok<MyResponse>, NotFound>>
{
public override void Configure() { ... }
public override async Task HandleAsync(MyRequest r, CancellationToken c)
{
if (true)
await SendResultAsync(TypedResults.Ok<MyResponse>(new(){ ... }));
else
await SendResultAsync(TypedResults.NotFound());
}
}
Configuring With Attributes
Instead of overriding the Configure() method, endpoint classes can be annotated with the following limited set of attributes:
- [Http{VERB}("/route")] - sets up the verb and route
- [AllowAnonymous] - allows un-authenticated access
- [AllowFileUploads] - allows file uploads with multipart/form-data
- [Authorize(...)] - specifies authorization requirements with roles and policies
- [Group<TGroup>] - associates an endpoint with a configuration group
- [PreProcessor<TProcessor>] - adds a pre-processor to the pipeline
- [PostProcessor<TProcessor>] - adds a post-processor to the pipeline
[HttpPost("/my-endpoint")]
[Authorize(Roles = "Admin,Manager")]
[PreProcessor<MyProcessor>]
public class MyEndpoint : Endpoint<MyRequest, MyResponse>
{
...
}
Any other attributes you place on the endpoint class will be automatically added to the endpoint metadata collection which would be the equivalent of the following when configuring endpoints inside the Configure() method:
Options(b => b.WithMetadata(new MyCustomAttribute()));
Advanced usage however does require overriding Configure(). You can only use one of these strategies. An exception will be thrown if you use both or none at all. When using Configure(), any custom attributes on the endpoints are ignored. Use WithMetadata as shown above instead.
Cancellation Token
The HandleAsync method of the endpoint is supplied a CancellationToken which you can pass down to your own async methods within the handler that requires a token.
The Send*Async methods of the endpoint also optionally accepts a CancellationToken. I.e. you can either pass down the same token supplied to the HandleAsync method or you may create/use a different token with these response sending methods depending on your requirement.
However, do note that it is not required to supply a CancellationToken to the Send*Async methods, and there's no real need to dirty up your code like the following:
await SendAsync(response, cancellation: ct);
Because if you do not supply the token to the Send*Async methods, the library automatically supplies the same token that is supplied to the HandleAsync method internally, and your code can remain cleaner.
The analyzer hint/warning can be turned off by adding the following to your csproj file:
<NoWarn>CA2016</NoWarn>