Spend less time over-engineering, and more time coding. The template has a focus on convenience, and developer confidence.
Want to see what a vertical slice looks like? Jump to the code snippet!
Important
This template is undergoing a rebuild, ready for version 2! ๐ฅณ See my experimental version 1 template here
Please wait patiently as this reaches the stable version, there's many important things to finish.
Please โญ the repository to show your support!
If you would like updates, feel free to 'Watch' the repo, that way you'll see the release in your GitHub home feed.
To install the template from NuGet.org run the following command:
dotnet new install Hona.VerticalSliceArchitecture.Template::2.0.0-rc6
Then create a new solution:
mkdir Sprout
cd Sprout
dotnet new hona-vsa
Finally, to update the template to the latest version run:
dotnet new update
dotnet new install Hona.VerticalSliceArchitecture.Template
then create:
var game = new Game(...);
game.MakeMove(0, 0, Tile.X);
game.MakeMove(0, 1, Tile.Y);
- with Vogen for Value-Objects
- with FluentResults for errors as values instead of exceptions
- For the Domain, start with an anemic Domain, then as use cases reuse logic, refactor into this more explicit Domain
[ValueObject<Guid>]
public readonly partial record struct GameId;
public class Game
{
public GameId Id { get; init; } = GameId.From(Guid.NewGuid());
...
- Use cases follow CQRS using Mediator (source gen alternative of MediatR)
- REPR pattern for the use cases
- 1 File per use case, containing the endpoint mapping, request, response, handler & application logic
- Endpoint is source generated
- For use cases, start with 'just get it working' style code, then refactor into the Domain/Common code.
- Mapster for source gen/explicit mapping, for example from Domain -> Response/ViewModels
Features/MyThings/MyQuery.cs
internal sealed record MyRequest(string Text);
internal sealed record MyResponse(int Result);
internal sealed class MyQuery(AppDbContext db)
: Endpoint<MyRequest, Results<Ok<GameResponse>, BadRequest>>
{
public override void Configure()
{
Get("/my/{Text}");
}
public override async Task HandleAsync(
MyRequest request,
CancellationToken cancellationToken
)
{
var thing = await db.Things.SingleAsync(x => x.Text == Text, cancellationToken);
if (thing is null)
{
await SendResultAsync(TypedResults.BadRequest());
return;
}
var output = new MyResponse(thing.Value);
await SendResultAsync(TypedResults.Ok(output));
}
}
- Common:
- EF Core, with fluent configuration
- This sample shows simple config to map a rich entity to EF Core without needing a data model (choose how you'd do this for your project)
Common/EfCore/AppDbContext.cs
public class AppDbContext : DbContext
{
public DbSet<MyEntity> MyEntities { get; set; } = default!;
...
}
Common/EfCore/Configuration/MyEntityConfiguration.cs
public class MyEntityConfiguration : IEntityTypeConfiguration<MyEntity>
{
public void Configure(EntityTypeBuilder<MyEntity> builder)
{
builder.HasKey(x => x.Id);
...
}
}
- Pre configured VSA architecture tests, using NuGet (Hona.ArchitectureTests). The template has configured which parts of the codebase relate to which VSA concepts. ๐
public class VerticalSliceArchitectureTests
{
[Fact]
public void VerticalSliceArchitecture()
{
Ensure.VerticalSliceArchitecture(x =>
{
x.Domain = new NamespacePart(SampleAppAssembly, ".Domain");
...
}).Assert();
}
}
- TODO:
- Add
MediatorFastEndpoints pipelines for cross cutting concerns on use cases, like logging, auth, validation (FluentValidation) etc (i.e. Common scoped to Use Cases)
- Add
Easy unit tests for the Domain layer
[Fact]
public void Game_MakeMove_BindsTile()
{
// Arrange
var game = new Game(GameId.From(Guid.NewGuid()), "Some Game");
var tile = Tile.X;
// Act
game.MakeMove(0, 0, tile);
// Assert
game.Board.Value[0][0].Should().Be(tile);
}
Easy integration tests for each Use Case or Command/Query
TODO: Test Containers, etc for integration testing the use cases. How does this tie into FastEndpoints now... :D
TODO
TODO: Section on mapping & how important the usages + used by at compile time is! (AM vs Mapperly)
The code is already architecture tested for VSA, but this is extensible, using Hona.ArchitectureTests
To demostrate the template, here is a current whole vertical slice/use case!
// ๐๐ป Vogen for Strong IDs + ๐๐ป 'GameId' field is hydrated from the route parameter
internal sealed record PlayTurnRequest(GameId GameId, int Row, int Column, Tile Player);
// ๐๐ป TypedResults for write once output as well as Swagger documentation
internal sealed class PlayTurnCommand(AppDbContext db)
: Endpoint<PlayTurnRequest, Results<Ok<GameResponse>, NotFound>>
{
// ๐๐ป FastEndpoints for a super easy Web API
public override void Configure()
{
Post("/games/{GameId}/play-turn");
Summary(x =>
{
x.Description = "Make a move in the game";
});
AllowAnonymous();
}
public override async Task HandleAsync(
PlayTurnRequest request,
CancellationToken cancellationToken
)
{
// ๐๐ป EF Core without crazy abstractions over the abstraction
var game = await db.FindAsync<Game>(request.GameId);
if (game is null)
{
await SendResultAsync(TypedResults.NotFound());
return;
}
// ๐๐ป Rich Domain for high value/shared logic
game.MakeMove(request.Row, request.Column, request.Player);
await db.SaveChangesAsync(cancellationToken);
// ๐๐ป Mapperly to easily get a view model with Usage chain at compile time
var output = GameResponse.MapFrom(game);
await SendResultAsync(TypedResults.Ok(output));
}
}
If you read it this far, why not give it a star? ;)