Here is how we organized the project:

  • The Data directory contains EF Core-related classes.
  • The Features directory contains the features. Each subfolder contains its underlying features (vertical slices), including controllers, exceptions, and other support classes required to implement the feature.
  • Each use case is self-contained and exposes the following classes:
  • Command or Query representing the MediatR request.
  • Result is the return value of that request.
  • MapperProfile instructs AutoMapper on how to map the use case-related objects (if any).
  • Validator contains the validation rules to validate the Command or Query objects (if any).
  • Handler contains the use case logic: how to handle the request.
  • The Models directory contains the domain model.

 Figure 17.4: Solution Explorer view of the file organizationFigure 17.4: Solution Explorer view of the file organization 

In this project, we support request validation using FluentValidation, a third-party NuGet package. We can also use System.ComponentModel.DataAnnotations or any other validation library that we want.

With FluentValidation, I find it easy to keep the validation within our vertical slice but outside the class we want to validate. The out-of-the-box .NET validation framework, DataAnnotations, does the opposite, forcing us to include the validation as metadata on the entities themselves. Both have pros and cons, but FluentValidation is easier to test and extend.

The following code is the Program.cs file. The highlighted lines represent registering FluentValidation and scanning the assembly to find validators:

var currentAssembly = typeof(Program).Assembly;
var builder = WebApplication.CreateBuilder(args);
builder.Services
    // Plumbing/Dependencies
    .AddAutoMapper(currentAssembly)
    .AddMediatR(o => o.RegisterServicesFromAssembly(currentAssembly))
    .AddSingleton(typeof(IPipelineBehavior<,>), typeof(ThrowFluentValidationExceptionBehavior<,>))
    // Data
    .AddDbContext<ProductContext>(options => options
        .UseInMemoryDatabase(“ProductContextMemoryDB”)
        .ConfigureWarnings(builder => builder.Ignore(InMemoryEventId.TransactionIgnoredWarning))
    )
    // Web/MVC
    .AddFluentValidationAutoValidation()
    .AddValidatorsFromAssembly(currentAssembly)
    .AddControllers()
;
var app = builder.Build();
app.MapControllers();
using (var seedScope = app.Services.CreateScope())
{
    var db = seedScope.ServiceProvider.GetRequiredService<ProductContext>();
    await ProductSeeder.SeedAsync(db);
}
app.Run();

The preceding code adds the bindings we explored in previous chapters, FluentValidation, and the other pieces required to run the application. The highlighted lines register FluentValidation and scan the currentAssembly for validator classes. The validators themselves are part of each vertical slice. Now that we covered the organization of the project, let’s look at features.