Passa al contenuto principale

Struttura della Solution

Struttura minima

NomeSoluzione/
├── NomeSoluzione.sln
├── src/
│ ├── NomeSoluzione.Core/ # Business logic, entità, interfacce
│ ├── NomeSoluzione.Db/ # EF DbContext, migration, configurazioni
│ └── NomeSoluzione.Api/ # ASP.NET Core — entry point HTTP
└── tests/
└── NomeSoluzione.Tests/ # Test di integrazione

Ogni progetto aggiuntivo (Worker, Console, Job) segue lo stesso schema: dipende da Core, non da Db direttamente salvo necessità.

Dipendenze

Api ──▶ Core ◀── Db
Tests ──▶ Core
Tests ──▶ Db

Core non dipende da nessuno. Db dipende da Core (implementa le sue interfacce). Api dipende da Core e registra le dipendenze concrete via DI.

Organizzazione interna di Core

Si organizza per dominio, non per tipo tecnico (Screaming Architecture):

NomeSoluzione.Core/
├── Ordini/
│ ├── Ordine.cs # Entity
│ ├── OrdineStato.cs # Enum di dominio
│ ├── CreaOrdine.cs # IUseCase
│ ├── ConfermaOrdine.cs # IUseCase
│ └── GestoreScorte.cs # Domain service
├── Clienti/
│ ├── Cliente.cs
│ └── RegistraCliente.cs
└── Shared/
├── IUseCase.cs
└── Result.cs

❌ Da evitare:

Core/
├── Services/
├── Repositories/
└── Models/

Convenzioni AI-friendly

Una solution ben strutturata è leggibile non solo dagli sviluppatori ma anche dall'IA che ci lavora. Le convenzioni che seguono massimizzano la comprensibilità contestuale senza richiedere spiegazioni aggiuntive.

Naming esplicito

I nomi descrivono l'intenzione, non il tipo:

// ✅ Il nome dice cosa fa
public class CreaOrdine : IUseCase<CreaOrdineCommand, OrdineId> { }
public class ConfermaOrdine : IUseCase<ConfermaOrdineCommand> { }
public class GestoreScorte { }

// ❌ Il nome dice cosa è, non cosa fa
public class OrdineService { }
public class OrdineManager { }

Un file, una responsabilità

Ogni file contiene una sola classe. Il nome del file coincide con il nome della classe.

CreaOrdine.cs → class CreaOrdine
OrdineStato.cs → enum OrdineStato
CreaOrdineCommand.cs → record CreaOrdineCommand

Record per comandi e DTO

I comandi e i DTO si esprimono come record, immutabili per costruzione:

public record CreaOrdineCommand(Guid ClienteId, IReadOnlyList<RigaOrdine> Righe);
public record RigaOrdine(Guid ProdottoId, int Quantita);

Interfacce solo dove necessario

Un'interfaccia si introduce quando serve sostituibilità reale (test, implementazioni multiple). Non si crea un'interfaccia per ogni classe per abitudine:

// ✅ Interfaccia che ha senso: più implementazioni possibili
public interface IEmailSender { Task SendAsync(Email email); }

// ❌ Interfaccia inutile: esiste solo GestoreScorte
public interface IGestoreScorte { }
public class GestoreScorte : IGestoreScorte { }

Program.cs minimal

Program.cs registra le dipendenze e avvia l'app. Non contiene logica:

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseSerilog((ctx, cfg) => cfg.ReadFrom.Configuration(ctx.Configuration));

builder.Services.AddDbContext<AppDbContext>(o =>
o.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

builder.Services.AddScoped<CreaOrdine>();
builder.Services.AddScoped<ConfermaOrdine>();
builder.Services.AddScoped<GestoreScorte>();

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

Per solution con molte dipendenze, si usano extension method per raggruppare le registrazioni:

// Infrastructure/ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddDomainServices(this IServiceCollection services)
{
services.AddScoped<CreaOrdine>();
services.AddScoped<ConfermaOrdine>();
services.AddScoped<GestoreScorte>();
return services;
}
}