.NET6 Incontra DDD

cleanarchitecture

.NET6 Incontra DDD

L’uscita della versione 6 del framework di casa Microsoft non è certo una novità a questo punto, e di articoli sulle novità introdotte da questa nuova versione ne è pieno il web, quindi, perchè un altro articolo su questo tema?

Ho letto molto riguardo le minimal API, su come il team di Visual Studio abbia ridotto considerevolmente il codice necessario per avviare una API riducendo il tutto ad un file. Sembra incredibile, ma l’aspetto del progetto per la creazione di una Web API in Visual Studio 2022 ora è questo

WebApplication
   appettings.json
   progam.cs
   WebApplication.csproj

Minimal API

In pratica niente più file startup.cs, niente più cartella Controller, ma un solo file, program.cs che ci permette di aggiungere tutte le feature necessarie in maniera estremamente pulita e semplice

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", => "Hello World!");

app.Run();

Non è certo quello a cui eravamo abituati noi amanti del framework di Microsoft, sicuramente troverà molte similitudini chi invece è abituato a sviluppare Web API sfruttando Node.js. Il fatto di avere un singolo file ci permette di aggiungere nuove features, (e nuovi endpoints) semplicemente modificando il file program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IWeatherService, WeatherService>();

var app  builder.Build();

app.MapPost("/weatherforecast", async (IWeatherService weatherService) =>
await weatherService.GetWeatherForecastAsync())
.WithName("GetWeatherForecast")
.WithTags("WeatherForecast");

app.Run();

Di esempi simili, e molto probabilmente più interessanti ne potete trovare quanti volete in rete, ma giunto a questo punto, mosso da curiosità e spinto da questo articolo ho intravisto un approccio più pulito allo sviluppo. Il fatto che ora sia possibile tralasciare la tradizionale struttura focalizzata più sugli aspetti tecnici dell’applicazione, ci permette di focalizzarci su un aspetto domain-driven, dove l’applicazione è strutturata attorno al Dominio.

Perchè un approccio DDD Oriented?

Domain-Driven Design non ha certo bisogno di presentazioni, e non vuole essere questo un articolo che spiega il DDD, anche su questo argomento potete trovare molti articoli in rete. Quello che a me interessa evidenziare in questo articolo è il ruolo dello sviluppo del software, ossia risolvere problemi di business, e non scrivere del buon codice fine a se stesso. DDD permette un approccio incrementale allo sviluppo, ossia ci permette, in estrema sintesi, di poter modificare in corso d’opera il nostro codice perchè le esigenze del nostro Cliente sono cambiate, oppure perchè ci siamo accorti che pensavamo di aver capito cosa fare, ed invece ci eravamo sbagliati ed ora lo dobbiamo riscrivere. Se nello sviluppo osserviamo i pattern suggeriti da E. Evans nel suo libro allora, possiamo stare tranquilli, far progredire la nostra soluzione senza dover pronunciare la maledetta frase “a questo punto è meglio riscrivere tutto” è possibile. Già, ma come possiamo organizzare la nostra solution affinché rispecchi un’architettura Clean?

.NET6 Incontra DDD

Molto spesso, se non praticamente sempre, è difficile capire cosa appartiene ad un Dominio, e cosa appartiene ad un altro sotto dominio. Affrontare lo sviluppo con i pattern del DDD significa imparare a conoscere il Dominio man mano che ci si lavora, e questo, a mio avviso, è l’aspetto vincente di questo approccio. Fornire soluzioni che servono allo scopo. Quindi, appurato questo, come suddivido in modo facile, ammesso ne esista uno, gli elementi della mia soluzione in modo da poterli facilmente spostare all’occorrenza?

Cos’è il Modulo?

L’idea è quella di organizzare le feature, o i sotto-domini, in Moduli, ossia in elementi che contengano tutto il necessario per rendere indipendente il nostro sotto-dominio. Cosa è esattamente un Modulo? Sostanzialmente una classe che espone due metodi, il primo necessario a configurare il nostro servizio di Dependency Injection, ed il secondo per registrare i relativi endpoints del modulo stesso. A questo punto avremo bisogno di un Modulo per ogni sotto dominio. Che vantaggio ci offre questo approccio?

Innanzitutto, nel momento in cui ci accorgiamo che un particolare sotto-dominio deve essere spostato in un altro Bouned Context, ossia un’altra Web API, ci basterà spostare i progetti, ed il relativo file module, per toglierli da questo Bounded Context e inserirli in quello nuovo.

Vediamo com’è fatto un Modulo

public sealed class WeatherModule
{
    public IServiceCollection RegisterWeatherModule(IServiceCollection services)
    {
        services.AddScoped<IWeatherService, WeatherService>();

        return services;
    }

    public IEndpointRouteBuilder MapWeatherEndpoints(IEndpointRouteBuilder endpoints)
    {
        endpoints.MapGet("v1/weatherforecast/", async (IWeatherService weatherService) => 
                await weatherService.GetWeatherForecastAsync())
            .WithName("GetWeatherForecast")
            .WithTags("WeatherForecast");

        return endpoints;
    }
}

E la relativa modifica al file program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.RegisterWeatherModule();

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

app.Run();

Arrivati a questo punto è facile intuire che per ogni sotto dominio ci dobbiamo creare la nostra classe DomainModule e poi registrarla, come visto poco sopra, nel file program.cs.

Così facendo però, perchè c’è sempre un però, spostare un modulo da una Web API ad un’altra ci obbliga a ricordarci di togliere e/o aggiungere la relativa registrazione nel file program.cs, onde evitare poi errori di compilazione. Come risolviamo questo dilemma?

Registriamo i Module

Innanzitutto definendo un’interfaccia per i nostri moduli, una IModule.cs

public interface IModule
{
    IServiceCollection RegisterModule(IServiceCollection services);
    IEndpointRouteBuilder MapEndpoints(IEndpointRouteBuilder endpoints);
}

E la nostra classe WeatherModule.cs ora viene riscritta in questo modo

public sealed class WeatherModule : IModule
{
    public IServiceCollection RegisterModule(IServiceCollection services)
    {
        services.AddWeatherModule();

        return services;
    }

    public IEndpointRouteBuilder MapEndpoints(IEndpointRouteBuilder endpoints)
    {
        endpoints.MapGet("v1/weatherforecast/", async (IWeatherService weatherService) => 
                await weatherService.GetWeatherForecastAsync())
            .WithName("GetWeatherForecast")
            .WithTags("WeatherForecast");

        return endpoints;
    }
}

Ovviamente questa operazione non è sufficiente, ma il fatto di avere una serie di Moduli che implementano la stessa interfaccia ci permette di scrivere un metodo che possa ricercarli tutti automaticamente, e magari registrali pure per noi in modo automatico. Allora vediamo come scrivere questo RegisterModules

public static class ModuleExtensions
{
    private static readonly IList<IModule> RegisteredModules = new List<IModule>();

    public static WebApplicationBuilder RegisterModules(this WebApplicationBuilder builder)
    {
        var modules = DiscoverModules();
        foreach (var module in modules)
        {
            module.RegisterModule(builder.Services);
            RegisteredModules.Add(module);
        }

        return builder;
    }

    public static WebApplication MapEndpoints(this WebApplication app)
    {
        foreach (var module in RegisteredModules)
        {
            module.MapEndpoints(app);
        }

        return app;
    }

    private static IEnumerable<IModule> DiscoverModules()
    {
        return typeof(IModule).Assembly
            .GetTypes()
            .Where(p => p.IsClass && p.IsAssignableTo(typeof(IModule)))
            .Select(Activator.CreateInstance)
            .Cast<IModule>();
    }
}

Ed infine, il nostro file program.cs verrà semplificato in questo modo

var builder = WebApplication.CreateBuilder(args);
builder.Services.RegisterModules();

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

Ora sì, ogni volta che toglieremo il relativo file DomainModule dal nostro progetto, il nostro program.cs eviterà di registrarlo, così come se aggiungiamo un nuovo modulo, lo stesso meccanismo lo registrerà per noi automaticamente.

Conclusioni

Certamente è possibile organizzare la propria solution in VisualStudio anche senza ricorrere alle Minimal API, ma questo nuovo approccio, molto semplice e assolutamente personalizzabile, ci aiuta enormemente.

Trovate il codice d’esempio qui

Vi consiglio inoltre la lettura di questi articoli

Tim Deschryver

Dan North e relativa risposta di Robert Martin (Uncle Bob)

Condividi questo post...

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *