For the past year I’ve been playing with Jimmy Bogard’s vertical slice architecture on a personal project and I became a fan. I recently recommended we start using it at work and after the initial positive feedback I was asked to prepare a bit of a pitch for my co-workers. So I decided to document my thoughts here. This will be mostly just a bunch of notes, references and personal observations to help me preach. If you find it useful, great! If you have anything to add or object to, please leave a comment. Thanks!
Naturally, I think the best pitch really is to just watch Jimmy explain it. I’ll leave these 2 videos here for your pleasure:
In very generic terms, this is how N-Tier is typically represented (image taken from Jimmy):
And here is an overview of how we currently structure our projects:
[Authorize]
.Net attribute
[ValidateModelState]
controller action attribute
OnActionExecuting()
IRepository<T>
such as:
ILookupRepository<T>
such as:
We use SPA’s on client-side. There, we already started grouping code by features a long time ago because it’s just much more convenient. Module routing, service code, controllers, templates, components most often all get grouped together inside a dedicated folder. VueJS and React even go as far to encourage putting controllers, templates and styles into a single file.
Vertical slice architecture that Jimmy Bogard describes is in a way similar to that - why would we not bundle things together on the backend?
A feature implemented using vertical slicing typically requires only 2 files to be touched / created:
Instead of using specific repositories:
Note how we no longer need all the service / repository interface files.
For complex, heavy domain logic that will see heavy reuse, it’s ok to use a separate *.Domain
project and to use use pure, rich domain models. These can then be shared by handlers. In many cases, this is not really necessary however as the logic is often simple enough to stay self-contained inside a handler - only introduce abstractions when there are pain-points.
Each feature is in a way isolated and self-contained.
Result<T>
based code flowpublic class GetProduct
{
public class Query : IRequest<Result<Model>>, IOwnerAuthorizedRequest
{
public int Id { get; set; }
public Query(int id)
{
Id = id;
}
}
public class OwnerAuthorizer : OwnerRequestAuthorizer<Command>
{
private readonly IRepository<ProductEntity> _productRepository;
public OwnerAuthorizer(IAppContext appContext, IRepository<ProductEntity> productRepository) : base(appContext)
{
_productRepository = productRepository;
}
protected override async Task<int?> GetResourceOwnerIdAsync(Command request)
{
var entity = await _productRepository.GetEntityAsync(request.Id);
return entity?.CreatedById;
}
}
public class Handler : IRequestHandler<Query, Result<Model>>
{
private readonly IRepository<ProductEntity> _productRepository;
public Handler(IRepository<ProductEntity> productRepository)
{
_productRepository = productRepository;
}
public async Task<Result<Model>> Handle(Query query, CancellationToken cancellationToken)
{
var bucket = new RelationPredicateBucket();
bucket.PredicateExpression.Add(ProductFields.Id == query.Id);
var entity = (await _productRepository.GetEntitiesAsync(bucket)).FirstOrDefault();
return ToModel(entity);
}
private static Result<Model> ToModel(ProductEntity entity)
{
if (entity == null)
{
return new NotFoundError();
}
var model = new Model
{
Id = entity.Id,
Name = entity.Name
};
return model;
}
}
public class Model
{
public int Id { get; set; }
public string Name { get; set; }
}
}
public class FilterProducts
{
public class Query : GridFilter, IRequest<Result<GridCollection<Model>>>, IAuthenticatedRequest
{
}
public class Handler : IRequestHandler<Query, Result<GridCollection<Model>>>
{
private readonly IAppContext _appContext;
private readonly IRepository<ProductEntity> _productRepository;
public Handler(IAppContext appContext, IRepository<ProductEntity> productRepository)
{
_appContext = appContext;
_productRepository = productRepository;
}
public async Task<Result<GridCollection<Model>>> Handle(Query query, CancellationToken cancellationToken)
{
var parameters = query.ToParameters();
var bucket = new RelationPredicateBucket();
bucket.PredicateExpression.Add(ProductFields.CreatedById == _appContext.Identity.User.Id);
var grid = await _productRepository.FetchGridEntitiesAsync(parameters, bucket);
return grid.ReMap(ToModel);
}
private static Model ToModel(ProductEntity entity)
{
if (entity == null)
{
return null;
}
var model = new Model
{
Id = entity.Id,
Name = entity.Name
};
return model;
}
}
public class Model
{
public int Id { get; set; }
public string Name { get; set; }
}
}
public class CreateProduct
{
public class Command : IRequest<Result<int>>, IAuthenticatedRequest
{
[Required]
public string Name { get; set; }
}
public class Handler : IRequestHandler<Command, Result<int>>
{
private readonly IAppContext _appContext;
private readonly IRepository<ProductEntity> _productRepository;
public Handler(IAppContext appContext, IRepository<ProductEntity> productRepository)
{
_appContext = appContext;
_productRepository = productRepository;
}
public async Task<Result<int>> Handle(Command command, CancellationToken cancellationToken)
{
var entity = new ProductEntity
{
CreatedById = _appContext.Identity.User.Id,
UpdatedById = _appContext.Identity.User.Id,
Name = command.Name
};
await _productRepository.SaveEntityAsync(entity, true);
return entity.Id;
}
}
}
public class UpdateProduct
{
public class Command : MediatR.IRequest<Result<Unit>>, IOwnerAuthorizedRequest
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
}
public class OwnerAuthorizer : OwnerRequestAuthorizer<Command>
{
private readonly IRepository<ProductEntity> _productRepository;
public OwnerAuthorizer(IAppContext appContext, IRepository<ProductEntity> productRepository) : base(appContext)
{
_productRepository = productRepository;
}
protected override async Task<int?> GetResourceOwnerIdAsync(Command request)
{
var entity = await _productRepository.GetEntityAsync(request.Id);
return entity?.CreatedById;
}
}
public class Handler : MediatR.IRequestHandler<Command, Result<Unit>>
{
private readonly IAppContext _appContext;
private readonly IRepository<ProductEntity> _productRepository;
public Handler(IAppContext appContext, IRepository<ProductEntity> productRepository)
{
_appContext = appContext;
_productRepository = productRepository;
}
public async Task<Result<Unit>> Handle(Command command, CancellationToken cancellationToken)
{
var entity = await _productRepository.GetEntityAsync(command.Id);
entity.UpdatedAt = DateTime.UtcNow;
entity.UpdatedById = _appContext.Identity.User.Id;
entity.Name = command.Name;
await _productRepository.SaveEntityAsync(entity);
return Result.Ok();
}
}
}