Skip to content

Commit

Permalink
Use MediatR and refactor cashier, barista and UI
Browse files Browse the repository at this point in the history
There's still a big change coming to the UI so it's easier for the cashier to use.
  • Loading branch information
fredimachado committed May 24, 2024
1 parent 3710d0c commit 775b6f3
Show file tree
Hide file tree
Showing 67 changed files with 646 additions and 438 deletions.
12 changes: 7 additions & 5 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,36 @@
<PackageVersion Include="Aspire.RabbitMQ.Client" Version="8.0.1" />
<PackageVersion Include="AspNetCore.HealthChecks.EventStore.gRPC" Version="6.0.1" />
<PackageVersion Include="AspNetCore.HealthChecks.Rabbitmq" Version="8.0.1" />
<PackageVersion Include="MediatR" Version="12.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.5" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="8.5.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.5.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.8.1" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.8.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.8.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.8.1" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.8.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.8.1" />
<PackageVersion Include="AntDesign" Version="0.15.5" />
<PackageVersion Include="Ardalis.GuardClauses" Version="4.5.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="coverlet.msbuild" Version="6.0.2" />
<PackageVersion Include="EventStore.Client.Grpc.Streams" Version="21.2.0" />
<PackageVersion Include="FakeItEasy" Version="8.2.0" />
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="FluentAssertions.Analyzers" Version="0.31.0" />
<PackageVersion Include="FluentAssertions.Analyzers" Version="0.32.0" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.5" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.5" />
<PackageVersion Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.5" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="MinVer" Version="4.3.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageVersion Include="MinVer" Version="5.0.0" />
<PackageVersion Include="NetArchTest.Rules" Version="1.3.2" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="ReflectionMagic" Version="5.0.1" />
<PackageVersion Include="Scrutor" Version="4.2.2" />
<PackageVersion Include="Shouldly" Version="4.2.1" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.6.1" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageVersion Include="TngTech.ArchUnitNET.xUnit" Version="0.10.6" />
<PackageVersion Include="xunit" Version="2.8.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.0" />
Expand Down
9 changes: 8 additions & 1 deletion NCafe.sln
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NCafe.ServiceDefaults", "sr
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aspire", "Aspire", "{81ABD551-6547-47EF-8F01-160466F018D2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.EventStore", "src\Aspire\Aspire.Hosting.EventStore\Aspire.Hosting.EventStore.csproj", "{1F5E7F20-3972-4E73-86A9-83F20F6EE8C7}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.EventStore", "src\Aspire\Aspire.Hosting.EventStore\Aspire.Hosting.EventStore.csproj", "{1F5E7F20-3972-4E73-86A9-83F20F6EE8C7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NCafe.Cashier.Api.Tests", "src\Cashier\NCafe.Cashier.Api.Tests\NCafe.Cashier.Api.Tests.csproj", "{BFBE126A-F7B7-4DFC-A4AF-962AD53A22C3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -159,6 +161,10 @@ Global
{1F5E7F20-3972-4E73-86A9-83F20F6EE8C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1F5E7F20-3972-4E73-86A9-83F20F6EE8C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1F5E7F20-3972-4E73-86A9-83F20F6EE8C7}.Release|Any CPU.Build.0 = Release|Any CPU
{BFBE126A-F7B7-4DFC-A4AF-962AD53A22C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BFBE126A-F7B7-4DFC-A4AF-962AD53A22C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BFBE126A-F7B7-4DFC-A4AF-962AD53A22C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BFBE126A-F7B7-4DFC-A4AF-962AD53A22C3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -187,6 +193,7 @@ Global
{ED16805D-2DEA-4F88-8BCD-E3460AE89CAF} = {81ABD551-6547-47EF-8F01-160466F018D2}
{A7B56906-8702-48CD-9CB0-2274802E54B2} = {81ABD551-6547-47EF-8F01-160466F018D2}
{1F5E7F20-3972-4E73-86A9-83F20F6EE8C7} = {81ABD551-6547-47EF-8F01-160466F018D2}
{BFBE126A-F7B7-4DFC-A4AF-962AD53A22C3} = {A45EA559-97E3-4460-A2E5-F385D0476851}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F0136BF1-D5F3-4E83-8D23-E3F43534FBB5}
Expand Down
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,10 @@ All common interfaces and abstractions are here. For example:
- **AggregateRoot**: Abstract base class for our domain entities
- **IEvent and Event**: Base for events that represent domain entity state changes
- **IRepository**: Contract used to fetch and save domain entities
- **IPublisher**: Contract used for publishing integration events
- **IBusPublisher**: Contract used for publishing integration events
- **MessageBus Events**: Integration events used to communicate between microservices
- **IProjectionService**: Contract used for building projections based on domain entity events
- **Read Model**: Contract used to fetch and save read models (projections)
- **Command, Query, Dispatchers and Handlers interfaces**
- **Basic exceptions**

So, basically, this project is the core of our solution and will only contain the
Expand Down Expand Up @@ -114,7 +113,7 @@ abstractions/interfaces (ex.: `IRepository`) instead of implementations
### Web API

The entry points (runners) of our microservices. They simply register the required dependencies using methods
from the Infrastructure project and map endpoints, which use `ICommandDispatcher` or `IQueryDispatcher` (see `Program.cs`).
from the Infrastructure project and map endpoints, which use `MediatR` to invoke a handler (see `Program.cs`).

These projects have a reference to its Domain, which should actually be called Application, to conform with Clean Architecture
since it contains use cases. The APIs also have a reference to the Infrastructure project.
Expand All @@ -123,8 +122,7 @@ Projection services can also be in the Web API project (Find more about projecti

In case the microservice needs to consume integration events, a Consumer service can be created
(see `OrdersConsumerService` in `Barista.Api`). Basically, this service implements .NET's `IHostedService`, subscribes
to a RabbitMQ stream specifying a queue, a topic and a callback, which in case will use `ICommandDispatcher` to, you guessed it,
dispatch a command to the domain.
to a RabbitMQ stream specifying a queue, a topic and a callback, which in case will use `MediatR` to invoke a domain command.

### Web UI

Expand All @@ -150,7 +148,7 @@ Implementations for all external dependencies are defined in this project. Like:
- EventStore Repository and Projection Service
- RabbitMQ publisher

There are some other implementations in here as well, like Command and Query dispatchers, a Logging decorator and
There are some other implementations in here as well, like a Logging decorator and
read model repositories (only in-memory for now).

This project also contains methods for registering all the implementations for interfaces defined
Expand Down
7 changes: 7 additions & 0 deletions src/Admin/NCafe.Admin.Api/NCafe.Admin.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,11 @@
<ProjectReference Include="..\NCafe.Admin.Domain\NCafe.Admin.Domain.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Update="MinVer">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
19 changes: 8 additions & 11 deletions src/Admin/NCafe.Admin.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
using MediatR;
using NCafe.Admin.Api.Projections;
using NCafe.Admin.Domain.Commands;
using NCafe.Admin.Domain.Queries;
using NCafe.Admin.Domain.ReadModels;
using NCafe.Core.Commands;
using NCafe.Core.Queries;
using NCafe.Infrastructure;

var builder = WebApplication.CreateBuilder(args);
Expand All @@ -12,14 +11,12 @@

// Add services to the container.
builder.Services.AddEventStoreRepository(builder.Configuration)
.AddCommandHandlers<CreateProduct>()
.AddCommandHandlerLogger()
.AddQueryHandlers<CreateProduct>();

builder.Services.AddInMemoryReadModelRepository<Product>()
.AddEventStoreProjectionService<Product>()
.AddInMemoryReadModelRepository<Product>()
.AddHostedService<ProductProjectionService>();

builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<CreateProduct>());

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

Expand Down Expand Up @@ -48,16 +45,16 @@

app.UseCors(corsPolicyName);

app.MapGet("/products", async (IQueryDispatcher queryDispatcher) =>
app.MapGet("/products", async (IMediator mediator) =>
{
var result = await queryDispatcher.QueryAsync(new GetProducts());
var result = await mediator.Send(new GetProducts());
return Results.Ok(result);
})
.WithName("GetProducts");

app.MapPost("/products", async (ICommandDispatcher commandDispatcher, CreateProduct command) =>
app.MapPost("/products", async (IMediator mediator, CreateProduct command) =>
{
await commandDispatcher.DispatchAsync(command);
await mediator.Send(command);
return Results.Created("/products", null);
})
.WithName("CreateProduct");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using NCafe.Admin.Domain.Entities;
using NCafe.Admin.Domain.Exceptions;
using NCafe.Core.Repositories;
using System.Threading;

namespace NCafe.Admin.Domain.Tests.Commands;

Expand All @@ -26,7 +27,7 @@ public async Task GivenInvalidName_ShouldThrowException(string name)
var command = new CreateProduct(name, 3);

// Act
var exception = await Record.ExceptionAsync(() => _sut.HandleAsync(command));
var exception = await Record.ExceptionAsync(() => _sut.Handle(command, CancellationToken.None));

// Assert
exception.ShouldBeOfType<InvalidProductNameException>();
Expand All @@ -41,7 +42,7 @@ public async Task GivenInvalidPrice_ShouldThrowException(decimal price)
var command = new CreateProduct("Flat White", price);

// Act
var exception = await Record.ExceptionAsync(() => _sut.HandleAsync(command));
var exception = await Record.ExceptionAsync(() => _sut.Handle(command, CancellationToken.None));

// Assert
exception.ShouldBeOfType<InvalidProductPriceException>();
Expand All @@ -54,10 +55,9 @@ public async Task GivenValidProductInformation_ShouldCreateAndStoreProduct()
var command = new CreateProduct("Flat White", 3.5m);

// Act
var exception = await Record.ExceptionAsync(() => _sut.HandleAsync(command));
await _sut.Handle(command, CancellationToken.None);

// Assert
exception.ShouldBeNull();
A.CallTo(() => _repository.Save(A<Product>.That.Matches(p => p.Name == command.Name && p.Price == command.Price)))
.MustHaveHappenedOnceExactly(); ;
}
Expand Down
21 changes: 5 additions & 16 deletions src/Admin/NCafe.Admin.Domain/Commands/CreateProduct.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
using NCafe.Admin.Domain.Entities;
using NCafe.Admin.Domain.Exceptions;
using NCafe.Core.Commands;
using MediatR;
using NCafe.Admin.Domain.Entities;
using NCafe.Core.Repositories;

namespace NCafe.Admin.Domain.Commands;

public record CreateProduct(string Name, decimal Price) : ICommand;
public record CreateProduct(string Name, decimal Price) : IRequest;

internal sealed class CreateProductHandler(IRepository repository) : ICommandHandler<CreateProduct>
internal sealed class CreateProductHandler(IRepository repository) : IRequestHandler<CreateProduct>
{
private readonly IRepository _repository = repository;

public async Task HandleAsync(CreateProduct command)
public async Task Handle(CreateProduct command, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(command.Name))
{
throw new InvalidProductNameException();
}

if (command.Price <= 0)
{
throw new InvalidProductPriceException(command.Price);
}

var product = new Product(Guid.NewGuid(), command.Name, command.Price);

await _repository.Save(product);
Expand Down
17 changes: 14 additions & 3 deletions src/Admin/NCafe.Admin.Domain/Entities/Product.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Ardalis.GuardClauses;
using NCafe.Admin.Domain.Events;
using NCafe.Admin.Domain.Exceptions;
using NCafe.Core.Domain;

namespace NCafe.Admin.Domain.Entities;
Expand All @@ -12,9 +13,19 @@ private Product()

public Product(Guid id, string name, decimal price)
{
Id = Guard.Against.Default(id, nameof(id));
Name = Guard.Against.NullOrWhiteSpace(name, nameof(name));
Price = Guard.Against.NegativeOrZero(price, nameof(price));
Id = Guard.Against.Default(id);
Name = name;
Price = price;

if (string.IsNullOrWhiteSpace(Name))
{
throw new InvalidProductNameException();
}

if (Price <= 0)
{
throw new InvalidProductPriceException(Price);
}

RaiseEvent(new ProductCreated(id, name, price));
}
Expand Down
5 changes: 2 additions & 3 deletions src/Admin/NCafe.Admin.Domain/NCafe.Admin.Domain.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@

<ItemGroup>
<PackageReference Include="Ardalis.GuardClauses" />
<PackageReference Include="MediatR" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Common\NCafe.Core\NCafe.Core.csproj" />
</ItemGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>$(AssemblyName).Tests</_Parameter1>
</AssemblyAttribute>
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
</ItemGroup>

</Project>
10 changes: 5 additions & 5 deletions src/Admin/NCafe.Admin.Domain/Queries/GetProducts.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
using NCafe.Admin.Domain.ReadModels;
using NCafe.Core.Queries;
using MediatR;
using NCafe.Admin.Domain.ReadModels;
using NCafe.Core.ReadModels;

namespace NCafe.Admin.Domain.Queries;

public record GetProducts : IQuery<Product[]>;
public record GetProducts : IRequest<Product[]>;

internal sealed class GetProductsHandler(IReadModelRepository<Product> productRepository) : IQueryHandler<GetProducts, Product[]>
internal sealed class GetProductsHandler(IReadModelRepository<Product> productRepository) : IRequestHandler<GetProducts, Product[]>
{
private readonly IReadModelRepository<Product> _productRepository = productRepository;

public Task<Product[]> HandleAsync(GetProducts query)
public Task<Product[]> Handle(GetProducts query, CancellationToken cancellation)
{
var products = _productRepository.GetAll()
.ToArray();
Expand Down
7 changes: 7 additions & 0 deletions src/Barista/NCafe.Barista.Api/NCafe.Barista.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,11 @@
<ProjectReference Include="..\NCafe.Barista.Domain\NCafe.Barista.Domain.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Update="MinVer">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
Loading

0 comments on commit 775b6f3

Please sign in to comment.