Skip to content

Spend less time over-engineering, and more time coding. The template has a focus on convenience, and developer confidence. Vertical Slice Architecture ๐ŸŽˆ

License

Notifications You must be signed in to change notification settings

Hona/VerticalSliceArchitecture

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

28 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

GitHub Repo stars GitHub contributors GitHub last commit GitHub commit activity open issues

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.

Getting started โšก

dotnet CLI

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

GUI

dotnet new install Hona.VerticalSliceArchitecture.Template

then create:

Features โœจ

A compelling example with the TikTacToe game! ๐ŸŽฎ

var game = new Game(...);
game.MakeMove(0, 0, Tile.X);
game.MakeMove(0, 1, Tile.Y);

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());

    ...

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));
    }
}

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);

        ...
    }
}

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();
    }
}

Cross Cutting Concerns

  • TODO:
    • Add Mediator FastEndpoints pipelines for cross cutting concerns on use cases, like logging, auth, validation (FluentValidation) etc (i.e. Common scoped to Use Cases)

Automated Testing

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);
}

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... :D

TODO

TODO: Section on mapping & how important the usages + used by at compile time is! (AM vs Mapperly)

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);

        // ๐Ÿ‘‡๐Ÿป 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? ;)

Repobeats analytics image

About

Spend less time over-engineering, and more time coding. The template has a focus on convenience, and developer confidence. Vertical Slice Architecture ๐ŸŽˆ

Topics

Resources

License

Stars

Watchers

Forks

Languages