Code, Learn

.NET 6 Incontra DDD

15 Marzo 2022 - 5 minuti di lettura
L’uscita di .NET 6, l’ultima versione del framework di casa Microsoft, ha destato molto interesse e di conseguenza si trovano diversi articoli sulle novità introdotte.
Ultimamente mi sono documentato molto sulla “Minimal API“, un’architettura che snellisce considerevolmente il codice necessario per avviare una API riducendo il tutto a un unico file.
Questo nuovo approccio per lo sviluppo delle API fornisce ad Alberto lo spunto per un’interessante riflessione che inizia dalla seguente domanda:
“E se considerassimo il Domain-Driven Design come approccio per lo sviluppo della nostra applicazione?”
Buona lettura.

Minimal API in .NET 6

Sembra incredibile, ma la struttura del progetto per la creazione di una Web API in Visual Studio 2022 ora è questa:

>>WebApplication
>>appettings.json
>>progam.cs
>>WebApplication.csproj

Non abbiamo più il file startup.cs e neppure la cartella Controller ma solamente il 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 ciò a cui eravamo abituati noi amanti del framework di Microsoft, sicuramente troverà molte similitudini chi invece è abituato a sviluppare Web API sfruttando Node.js. La presenza di un singolo file ci permette di aggiungere nuove feature (e nuovi endpoint) 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();

In rete potete trovare esempi simili e forse più interessanti, ma il punto è un altro:

La grande novità delle Minimal API sta tutta nel fatto che ora abbiamo solo il file program.cs o c’è dell’altro?“.

Dover gestire più file comporta da sempre più lavoro per gli sviluppatori e l’IDE Visual Studio, quando si apre una “solution”, carica tutti i file in automatico. Giunto a questo punto, mosso da curiosità e spinto dalla lettura dell’articolo di Tim Deschryver 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 concentrarci su un aspetto “domain-driven“, dove l’applicazione è strutturata attorno al Dominio.

Perché un approccio “DDD Oriented”?

Domain-Driven Design (DDD) non ha certo bisogno di presentazioni, esistono diversi libri e articoli che trattano approfonditamente la tematica. Ciò che mi interessa evidenziare in questo articolo è il ruolo dello sviluppo del software, ossia risolvere problemi di business, e non scrivere del buon codice fine a sé stesso. DDD sposta l’attenzione sui processi di dominio piuttosto che sui dati, portandoci a riflettere sui tecnicismi il più tardi possibile; quello che emerge da un processo di confronto sul modello di dominio sono le priorità delle feature da implementare, ossia il valore che il business desidera venga implementato. Per noi sviluppatori una feature vale l’altra, è solo codice da implementare. DDD invece ci impone di mantenere la mente aperta e pronta ad ascoltare i requisiti, e lasciare che il modello di dominio si sviluppi il più vicino possibile a quello che il nostro business expert desidera. La parte complicata, quando si sviluppa una soluzione cercando di applicare i pattern del DDD, è la difficoltà iniziale nell’implementare una soluzione che possa andare bene sin da subito. È praticamente impossibile! La soluzione si deve continuamente adeguare alla conoscenza che lo sviluppatore acquisisce del dominio, e questo avviene solo lavorando su di essa (in realtà qualche tool di modellazione molto pratico esiste, magari ne parlerò in un altro articolo). Abbiamo quindi l’esigenza di scrivere codice che sia facilmente malleabile e plasmabile.
Ciò detto…

Come si potrebbe strutturare il codice in modo che possa soddisfare le caratteristiche di cui vi ho appena parlato?

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:

  • Configurazione del servizio di Dependency Injection.
  • Registrazione dei relativi endpoint del modulo stesso.

A questo punto ci sarà bisogno di un Modulo per ogni sotto dominio. Che vantaggio ci offre questo approccio?
Innanzitutto, nel momento in cui ci si accorge che un particolare sotto-dominio debba essere spostato in un altro Bounded Context, ossia un’altra Web API, basterà spostare i progetti – e il relativo file module – per toglierli da questo Bounded Context e inserirli in quello nuovo.

Di seguito è mostrato il codice di 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.app();
app.MapWEatherEndpoints();
app.Run();

Così facendo però, perché c’è sempre un però, spostare un modulo da una Web API a 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 definiamo un’interfaccia per i nostri moduli, IModule.cs

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

La nostra classe WeatherModule.cs viene così riscritta:

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 registrarli pure per noi in modo automatico. Vediamo quindi come scrivere questo registratore di Moduli, RegisterModules:

public static class ModuleExtensions
{
    
    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 DiscoverModules()
    {
        return typeof(IModule).Assembly
            .GetTypes()
    	    .Where(p => p.IsClass && p.IsAssignableTo(typeof(IModule)))
            .Select(Activator.CreateInstance)
            .Cast();
    }
}

Con queste modifiche il file program.cs verrà semplificato come segue:

var builder = WebApplication.CreateBuilder(args);
{
    IServiceCollection RegisterModule(IServiceCollection services);
    IEndpointRouteBuilder MapEndpoints(IEndpointRouteBuilder endpoints);
}

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

Conclusioni

Certamente è possibile organizzare la propria solution in Visual Studio anche senza ricorrere alle Minimal API, ma questo nuovo approccio, molto semplice e assolutamente personalizzabile, ci aiuta enormemente.
Trovate tutto il codice di esempio in questo repository del mio GitHub.
Per ulteriori approfondimenti vi consiglio la lettura dei seguenti articoli:
Articolo scritto da