Przejdź do treści

FastEndpoints

  • przez

W tym poście chciałbym pokazać bibliotekę FastEndpoints, która jest alternatywą dla minimal API i MVC w .NET. Autorzy twierdzą, że ich rozwiązanie jest szybsze i używa mniej pamięci niż klasyczny kontroler MVC.

Twórcy udostępnili także rozszerzenia dla Visual Studio oraz Visual Studio Code. Dokumentacja biblioteki znajduje się tutaj. Zacznijmy od utworzenia projektu:

dotnet new web -n FastEndpointsDemo
cd FastEndpointsDemo
dotnet add package FastEndpoints --version 5.25.0
dotnet add package FastEndpoints.Swagger --version 5.25.0
dotnet add package FastEndpoints.Generator --version 5.25.0
dotnet add package NMemory

Uwaga🚨Czasem może się pojawić następujący błąd:

FastEndpoints.Generator.AccessControlGenerator\Allow.b.g.cs(14,125): error CS0246: The type or namespace name 'HideFromDocsAttribute' could not be found

Jeśli macie taki błąd to sprawdźcie czy macie zainstalowany .NET Compiler Platform SDK

Nasz projekt będzie zawierał endpointy CRUD dla klasy Person:

public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public int Age { get; set; } = 0;
    public DateTime CreatedAt { get; set; }
    public DateTime EditedAt { get; set; }
}

Cała konfiguracja odbywa się w Program.cs

using FastEndpoints;
using FastEndpoints.Swagger;
using FastEndpointsDemo;


var builder = WebApplication.CreateBuilder();
builder.Services
   .AddFastEndpoints(o =>
   {
      o.SourceGeneratorDiscoveredTypes.AddRange(DiscoveredTypes.All);
   })
   .SwaggerDocument(o =>
{
   o.DocumentSettings = s =>
   {
      s.Title = "FastEncpoints Demo API";
      s.Version = "v1";
   };
});
builder.Services.AddSingleton<DemoDatabase>();

var app = builder.Build();
app.UseFastEndpoints()
   .UseSwaggerGen();
app.Run();

Baza danych

W tym projekcie użyję NMemory. Ta biblioteka będzie naszą bazą danych, która działa w pamięci (po restarcie api dane są usuwane).

using NMemory;
using NMemory.Tables;

namespace FastEndpointsDemo;

public class DemoDatabase : Database
{
    public DemoDatabase()
    {
        this.Persons = this.Tables.Create<Person, int>(x => x.Id, new IdentitySpecification<Person>(x => x.Id, 1, 1));
    }

    public ITable<Person> Persons { get; }

}

Musimy też dodać naszą bazę do DI

builder.Services.AddSingleton<DemoDatabase>();

Endpointy

Wszystkie dostępne typy endpointów są opisane tutaj. Ja użyję Endpoint oraz EndpointWithoutRequest

Create

Zacznijmy od endpointu dla do dodawania nowego obiektu, który będzie dostępny pod url POST /api/user

Nasz wymagany request to:

namespace FastEndpointsDemo;

public class CreatePersonRequest
{
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public int Age { get; set; } = 0;
}

To teraz dodajmy endpoint dla POST. W tym celu nasza klasa musi dziedziczyć z Endpoint<CreatePersonRequest>

using FastEndpoints;

namespace FastEndpointsDemo;

public class CreatePersonEndpoint : Endpoint<CreatePersonRequest>
{
    public override void Configure()
    {
        Post("/api/user");
        AllowAnonymous();
    }

    public override async Task HandleAsync(CreatePersonRequest rq, CancellationToken ct)
    {
        await PublishAsync(new CreatePersonEvent
        {
            FirstName = rq.FirstName,
            LastName = rq.LastName,
            Age = rq.Age
        });

        await SendOkAsync();
    }
}

FastEndpoint zawiera także event bus, który jest opisany tutaj. W powyższym endpoincie używam metody PublishAsync właśnie z tej funkcjonalności. A więc dodajmy event i handler dla eventu:

namespace FastEndpointsDemo;

public class CreatePersonEvent
{
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public int Age { get; set; } = 0;
}
using FastEndpoints;

namespace FastEndpointsDemo;

public class CreatePersonHandler : IEventHandler<CreatePersonEvent>
{
    private readonly DemoDatabase _db;

    public CreatePersonHandler(DemoDatabase db)
    {
        _db = db;
    }

    public Task HandleAsync(CreatePersonEvent eventModel, CancellationToken ct)
    {
        _db.Persons.Insert(
            new Person
            {
                FirstName = eventModel.FirstName,
                LastName = eventModel.LastName,
                Age = eventModel.Age,
                CreatedAt = DateTime.Now,
            });

        return Task.CompletedTask;
    }
}

W podobny sposób dodamy resztę naszych endpointów.

Delete

Dla endpointa DELETE potrzebujemy, żeby id było w route (/api/person/{id}). FastEndpoint sam zmapuje id z route do property Id klasy DeletePersonRequest.

namespace FastEndpointsDemo;

public class DeletePersonRequest
{
    public int Id { get; set; }
}

Endpoint dla DELETE będzie dziedziczyć po Enpoint<DeletePersonRequest>.

using FastEndpoints;

namespace FastEndpointsDemo;

public class DeletePersonEndpoint : Endpoint<DeletePersonRequest>
{
    public override void Configure()
    {
        Delete("/api/person/{id}");
        AllowAnonymous();
    }

    public override async Task HandleAsync(DeletePersonRequest rq, CancellationToken ct)
    {
        await PublishAsync(new DeletePersonEvent { Id = rq.Id });
        await SendOkAsync();
    }
}

Podobnie jak dla CREATE użyjemy eventu i handlera.

namespace FastEndpointsDemo;

public class DeletePersonEvent
{
    public int Id { get; set; }
}
using FastEndpoints;

namespace FastEndpointsDemo;

public class DeletePersonHandler : IEventHandler<DeletePersonEvent>
{
    private readonly DemoDatabase _db;

    public DeletePersonHandler(DemoDatabase db)
    {
        _db = db;
    }

    public Task HandleAsync(DeletePersonEvent eventModel, CancellationToken ct)
    {
        var person = _db.Persons.First(x => x.Id == eventModel.Id);
        _db.Persons.Delete(person);

        return Task.CompletedTask;
    }
}

Get

GET będzie zwracał wszystkie Person więc możemy użyć EndpointWithoutRequest<PersonResponse>.

using FastEndpoints;

namespace FastEndpointsDemo;

public class GetPersonsEndpoint : EndpointWithoutRequest<PersonsResponse>
{
    private readonly DemoDatabase _db;

    public GetPersonsEndpoint(DemoDatabase db)
    {
        _db = db;
    }

    public override void Configure()
    {
        Get("/api/person");
        AllowAnonymous();
    }

    public override async Task HandleAsync(CancellationToken ct)
    {
        var rs = new PersonsResponse
        {
            Persons = _db.Persons.ToList()
        };

        await SendOkAsync(rs);
    }
}
namespace FastEndpointsDemo;

public class PersonsResponse
{
    public IList<Person> Persons { get; set; } = new List<Person>();
}

Update

Logika dla UPDATE jest podobna jak dla CREATE i DELETE.

namespace FastEndpointsDemo;

public class UpdatePersonRequest : CreatePersonRequest
{
    public int Id { get; set; }
}
using FastEndpoints;

namespace FastEndpointsDemo;

public class UpdatePersonEndpoint : Endpoint<UpdatePersonRequest>
{
    public override void Configure()
    {
        Put("/api/person");
        AllowAnonymous();
    }

    public override async Task HandleAsync(UpdatePersonRequest rq, CancellationToken ct)
    {
        await PublishAsync(
            new UpdatePersonEvent
            {
                Id = rq.Id,
                LastName = rq.LastName,
                FirstName = rq.FirstName,
                Age = rq.Age
            });
        await SendOkAsync();
    }
}
namespace FastEndpointsDemo;

public class UpdatePersonEvent
{
    public int Id { get; set; }
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public int Age { get; set; } = 0;
}
using FastEndpoints;

namespace FastEndpointsDemo;

public class UpdatePersonHandler : IEventHandler<UpdatePersonEvent>
{
    private readonly DemoDatabase _db;

    public UpdatePersonHandler(DemoDatabase db)
    {
        _db = db;
    }

    public Task HandleAsync(UpdatePersonEvent eventModel, CancellationToken ct)
    {
        var person = _db.Persons.First(x => x.Id == eventModel.Id);
        person.Age = eventModel.Age;
        person.EditedAt = DateTime.Now;
        person.FirstName = eventModel.FirstName;
        person.LastName = eventModel.LastName;
        _db.Persons.Update(person);

        return Task.CompletedTask;
    }
}

Swagger

I to wszystko. Teraz po uruchomieniu api pod adresem http://localhost:5050/swagger/index.html#/Api powinieneś zobaczy poniższy obrazek

Kod źródłowy

Kod źródłowy znajduje się pod adresem https://github.com/letyshub/FastEndpointDemo

Podsumowanie

FastEndpoints jest biblioteką godną rozważenia. Polecam Ci zapoznać się z dokumentacją. Ta biblioteka posiada wszystko co jest potrzebne do stworzenia dobrego API m. in. walidatory, exception handlery, autentykację/autoryzację, rate limiting, cache itp. itd. 🙂