“E se considerassimo il Domain-Driven Design come approccio per lo sviluppo della nostra applicazione?”
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”?
“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
- “Maybe it’s time to rethink our project structure with .NET 6“, di Tim Deschryver.
- “CUPID – the back story” di Dan North e “Solid Relevance“, la relativa risposta a cura di Robert Martin (più noto come Uncle Bob).