Vertical Slice Architecture en .NET: casos de uso sin interfaces ni repositorios
La segunda parte de la serie muestra cómo estructurar un caso de uso en un único archivo, eliminar interfaces ceremoniales y usar EF Core sin repositorio genérico.

En la segunda entrega de la serie el autor abre el "capó" de una implementación real en .NET y responde a las dudas más habituales al adoptar Vertical Slice Architecture (VSA). El ejemplo gira alrededor del caso de uso Crear Producto y muestra cómo todo lo necesario –DTOs, validación y lógica– vive en el mismo archivo Features/Products/CreateProduct.cs.
using FluentValidation;
using Microsoft.AspNetCore.Http.HttpResults;
using VsaTemplate.Common.Events;
using VsaTemplate.Common.Persistence;
using VsaTemplate.Features.Products.Contracts;
using VsaTemplate.Features.Products.Domain;
namespace VsaTemplate.Features.Products;
public static class CreateProduct {
public record Request(string Name, decimal Price);
public record Response(Guid Id, string Name, decimal Price);
public class Validator : AbstractValidator<Request> {
public Validator() {
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
RuleFor(x => x.Price).GreaterThan(0);
}
}
}
public class CreateProductHandler(AppDbContext db, IEventDispatcher eventDispatcher) {
public async Task<Created<CreateProduct.Response>> Handle(CreateProduct.Request request, CancellationToken ct) {
var product = new Product { Id = Guid.NewGuid(), Name = request.Name.Trim(), Price = request.Price };
db.Products.Add(product);
await db.SaveChangesAsync(ct);
await eventDispatcher.Publish(new ProductCreated(product.Name, product.Price), ct);
return TypedResults.Created($"/products/{product.Id}", new CreateProduct.Response(product.Id, product.Name, product.Price));
}
}
Sin interfaces para los handlers
En proyectos tradicionales de .NET suele esperarse una interfaz como ICreateProductHandler para registrar el handler en el contenedor de dependencias. El artículo argumenta que, dado que cada caso de uso tiene una única implementación, la interfaz solo añade ceremonia sin aportar flexibilidad. La testabilidad se mantiene porque las dependencias (AppDbContext, IEventDispatcher) se inyectan directamente en el constructor, lo que permite pasar implementaciones en memoria o falsas en los tests.
Uso de interfaces solo donde hay alternativas reales
El autor aclara que las interfaces siguen siendo útiles en los límites del sistema, por ejemplo para abstraer proveedores de tiempo, despachadores de eventos, pasarelas de pago o clientes de correo. En esos puntos sí pueden existir varias implementaciones y la inyección de dependencias aporta valor.
Registro automático con Scrutor
Para evitar registrar cada handler a mano, se emplea Scrutor. Un método de extensión escanea el ensamblado y registra todas las clases públicas cuyo nombre termina en Handler como sí mismas:
public static IServiceCollection AddUseCaseHandlers(this IServiceCollection services) =>
services.Scan(scan => scan
.FromAssemblyOf<Program>()
.AddClasses(classes => classes.Where(t => t.IsPublic && !t.IsAbstract && t.Name.EndsWith("Handler")))
.AsSelf()
.WithScopedLifetime());
Persistencia sin repositorio genérico
En lugar de un patrón de repositorio genérico, el handler accede directamente al DbContext. Con EF Core, este enfoque evita la sobrecarga de capas intermedias y mantiene la lógica de negocio cerca de los datos que manipula.
El artículo concluye que VSA permite una mayor cohesión espacial: todo lo que cambia junto, vive junto. Un ajuste como ampliar la longitud del nombre del producto se realiza editando un único archivo, sin tocar capas externas.
Para explorar el proyecto completo consulte el repositorio en GitHub.


