Hona.VerticalSliceArchitecture.Template
2.0.0-rc4
See the version list below for details.
dotnet new install Hona.VerticalSliceArchitecture.Template::2.0.0-rc4
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!
<p align="center"> <img src="docs/divider-primary.png" /> </p>
[!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.
<p align="center"> <img src="docs/divider-primary.png" /> </p>
<h3 align="center"><strong>Getting started ⚡</strong></h3>
<p align="center"> <img src="docs/divider-primary.png" /> </p>
dotnet CLI
To install the template from NuGet.org run the following command:
dotnet new install Hona.VerticalSliceArchitecture.Template
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
GUI
dotnet new install Hona.VerticalSliceArchitecture.Template
then create:
<img src="https://github.com/user-attachments/assets/797575c2-1304-4501-b9d2-6b6863ace0f3" width="500" />
<h3 align="center"><strong>Features ✨</strong></h3>
<p align="center"> <img src="docs/divider-primary.png" /> </p>
A compelling example with the TikTacToe game! 🎮
var game = new Game(...);
game.MakeMove(0, 0, Tile.X);
game.MakeMove(0, 1, Tile.Y);
<p align="center"> <img src="docs/divider-secondary.png" /> </p>
Rich Domain (thank you DDD!)
- 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());
...
<p align="center"> <img src="docs/divider-secondary.png" /> </p>
Quick to write feature slices
- 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));
}
}
<p align="center"> <img src="docs/divider-secondary.png" /> </p>
EF Core
- 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);
...
}
}
<p align="center"> <img src="docs/divider-secondary.png" /> </p>
Architecture Tests via NuGet package
- 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();
}
}
<p align="center"> <img src="docs/divider-secondary.png" /> </p>
Cross Cutting Concerns
- 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
Automated Testing
<p align="center"> <img src="docs/divider-tertiary.png" /> </p>
Domain - Unit Tested
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);
}
<p align="center"> <img src="docs/divider-secondary.png" /> </p>
Application - Integration Tested
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... 😄
TODO
<p align="center"> <img src="docs/divider-secondary.png" /> </p>
Code - Architecture Tested
The code is already architecture tested for VSA, but this is extensible, using Hona.ArchitectureTests
Full Code Snippet
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);
// 👇🏻 Mapster to easily get a view model
var output = game.Adapt<GameResponse>();
await SendResultAsync(TypedResults.Ok(output));
}
}
If you read it this far, why not give it a star? 😉
<p align="center"> <img src="docs/divider-tertiary.png" /> </p>
<p align="center"> <img src="https://repobeats.axiom.co/api/embed/5491ee3c19266af4f681dcf016c299dfc2973b5f.svg" alt="Repobeats analytics image" /> </p>
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
2.0.0-rc6 | 123 | 10/3/2024 |
2.0.0-rc5 | 83 | 7/30/2024 |
2.0.0-rc4 | 52 | 7/30/2024 |
2.0.0-rc3 | 40 | 7/30/2024 |
2.0.0-rc2 | 49 | 7/30/2024 |
2.0.0-rc1 | 55 | 7/30/2024 |
2.0.0-beta1 | 55 | 7/30/2024 |
2.0.0-alpha1 | 87 | 7/22/2024 |
0.8.0 | 617 | 3/23/2024 |
0.7.4 | 229 | 3/20/2024 |
0.7.3 | 180 | 3/20/2024 |
0.7.2 | 167 | 3/20/2024 |
0.7.1 | 128 | 3/20/2024 |
0.7.0 | 188 | 3/20/2024 |
0.6.0 | 291 | 3/14/2024 |
0.5.3 | 2,487 | 12/22/2023 |
0.5.2 | 219 | 12/22/2023 |
0.5.1 | 195 | 12/22/2023 |
0.5.0 | 189 | 12/22/2023 |
0.4.0 | 231 | 12/21/2023 |
0.3.0 | 1,359 | 11/15/2023 |
0.2.1 | 986 | 10/22/2023 |
0.2.0 | 332 | 10/22/2023 |
0.1.4 | 314 | 9/20/2023 |
0.1.3 | 289 | 9/20/2023 |
0.1.2 | 320 | 9/20/2023 |
0.1.1 | 297 | 9/20/2023 |
Early preview of my v2 VSA template.