Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CQRS with Minimal APIs sample #92

Merged
merged 4 commits into from
May 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions EventSourcing.NetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "13-Projections.SingleStream
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "14-Projections.SingleStream.EventualConsistency", "Workshops\IntroductionToEventSourcing\Solved\14-Projections.SingleStream.EventualConsistency\14-Projections.SingleStream.EventualConsistency.csproj", "{A5A2A4CB-08F6-49AE-B044-DD9C5F8B0F48}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Warehouse.MinimalAPI", "Warehouse.MinimalAPI", "{005FC7CE-97BA-47F7-982E-63C46327F78C}"
ProjectSection(SolutionItems) = preProject
Sample\Warehouse.MinimalAPI\docker-compose.yml = Sample\Warehouse.MinimalAPI\docker-compose.yml
Sample\Warehouse.MinimalAPI\README.md = Sample\Warehouse.MinimalAPI\README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Warehouse.Api", "Sample\Warehouse.MinimalAPI\Warehouse.Api\Warehouse.Api.csproj", "{723AD8CE-E3EF-40F6-9576-CCF2C3DB504E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -737,6 +745,10 @@ Global
{A5A2A4CB-08F6-49AE-B044-DD9C5F8B0F48}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A5A2A4CB-08F6-49AE-B044-DD9C5F8B0F48}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A5A2A4CB-08F6-49AE-B044-DD9C5F8B0F48}.Release|Any CPU.Build.0 = Release|Any CPU
{723AD8CE-E3EF-40F6-9576-CCF2C3DB504E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{723AD8CE-E3EF-40F6-9576-CCF2C3DB504E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{723AD8CE-E3EF-40F6-9576-CCF2C3DB504E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{723AD8CE-E3EF-40F6-9576-CCF2C3DB504E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -867,6 +879,8 @@ Global
{2CA4B0B3-AF8B-4BD8-A18D-18EE1F93DE04} = {65F6E2BE-B2D4-4E56-B0CB-3062C4882B9E}
{7A990FD1-4935-4F85-912C-D4B870232856} = {65F6E2BE-B2D4-4E56-B0CB-3062C4882B9E}
{A5A2A4CB-08F6-49AE-B044-DD9C5F8B0F48} = {65F6E2BE-B2D4-4E56-B0CB-3062C4882B9E}
{005FC7CE-97BA-47F7-982E-63C46327F78C} = {A7186B6B-D56D-4AEF-B6B7-FAA827764C34}
{723AD8CE-E3EF-40F6-9576-CCF2C3DB504E} = {005FC7CE-97BA-47F7-982E-63C46327F78C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A5F55604-2FF3-43B7-B657-4F18E6E95D3B}
Expand Down
33 changes: 22 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ Tutorial, practical samples and other resources about Event Sourcing in .NET. Se
- [6.2 Simple EventSourcing with EventStoreDB](#62-simple-eventsourcing-with-eventstoredb)
- [6.3 ECommerce with EventStoreDB](#63-ecommerce-with-eventstoredb)
- [6.5 Warehouse](#65-warehouse)
- [6.6 Event Versioning](#66-event-versioning)
- [6.7 Event Pipelines](#67-event-pipelines)
- [6.8 Meetings Management with Marten](#68-meetings-management-with-marten)
- [6.9 Cinema Tickets Reservations with Marten](#69-cinema-tickets-reservations-with-marten)
- [6.10 SmartHome IoT with Marten](#610-smarthome-iot-with-marten)
- [6.6 Warehouse Minimal API](#66-warehouse-minimal-api)
- [6.7 Event Versioning](#67-event-versioning)
- [6.8 Event Pipelines](#68-event-pipelines)
- [6.9 Meetings Management with Marten](#69-meetings-management-with-marten)
- [6.10 Cinema Tickets Reservations with Marten](#610-cinema-tickets-reservations-with-marten)
- [6.11 SmartHome IoT with Marten](#611-smarthome-iot-with-marten)
- [7. Self-paced training Kits](#7-self-paced-training-kits)
- [7.1 Introduction to Event Sourcing](#71-introduction-to-event-sourcing)
- [7.2 Build your own Event Store](#72-build-your-own-event-store)
Expand Down Expand Up @@ -652,7 +653,13 @@ Samples are using CQRS architecture. They're sliced based on the business module
- No Event Sourcing! Using Entity Framework to show that CQRS is not bounded to Event Sourcing or any type of storage,
- No Aggregates! CQRS do not need DDD. Business logic can be handled in handlers.

### 6.6 [Event Versioning](./Sample/EventsVersioning)
### 6.6 [Warehouse Minimal API](./Sample/Warehouse.MinimalAPI/)
Variation of the previous example, but:
- using Minimal API,
- example how to inject handlers in MediatR like style to decouple API from handlers.
- 📝 Read more [CQRS is simpler than you think with .NET 6 and C# 10](https://event-driven.io/en/cqrs_is_simpler_than_you_think_with_net6/?utm_source=event_sourcing_net)

### 6.7 [Event Versioning](./Sample/EventsVersioning)
Shows how to handle basic event schema versioning scenarios using event and stream transformations (e.g. upcasting):
- [Simple mapping](./Sample/EventsVersioning/#simple-mapping)
- [New not required property](./Sample/EventsVersioning/#new-not-required-property)
Expand All @@ -665,8 +672,9 @@ Shows how to handle basic event schema versioning scenarios using event and stre
- [Events Transformations](./Sample/EventsVersioning/#events-transformations)
- [Stream Transformation](./Sample/EventsVersioning/#stream-transformation)
- [Summary](./Sample/EventsVersioning/#summary)
- 📝 [Simple patterns for events schema versioning](https://event-driven.io/en/simple_events_versioning_patterns/?utm_source=event_sourcing_net)

### 6.7 [Event Pipelines](./Sample/EventPipelines)
### 6.8 [Event Pipelines](./Sample/EventPipelines)
Shows how to compose event handlers in the processing pipelines to:
- filter events,
- transform them,
Expand All @@ -676,21 +684,22 @@ Shows how to compose event handlers in the processing pipelines to:
- allows using interfaces and classes if you want to,
- can be used with Dependency Injection, but also without through builder,
- integrates with MediatR if you want to.
- 📝 Read more [How to build a simple event pipeline](https://event-driven.io/en/how_to_build_simple_event_pipeline/?utm_source=event_sourcing_net)

### 6.8 [Meetings Management with Marten](./Sample/MeetingsManagement/)
### 6.9 [Meetings Management with Marten](./Sample/MeetingsManagement/)
- typical Event Sourcing and CQRS flow,
- DDD using Aggregates,
- microservices example,
- stores events to Marten,
- Kafka as a messaging platform to integrate microservices,
- read models handled in separate microservice and stored to other database (ElasticSearch)

### 6.9 [Cinema Tickets Reservations with Marten](./Sample/Tickets/)
### 6.10 [Cinema Tickets Reservations with Marten](./Sample/Tickets/)
- typical Event Sourcing and CQRS flow,
- DDD using Aggregates,
- stores events to Marten.

### 6.10 [SmartHome IoT with Marten](./Sample/AsyncProjections/)
### 6.11 [SmartHome IoT with Marten](./Sample/AsyncProjections/)
- typical Event Sourcing and CQRS flow,
- DDD using Aggregates,
- stores events to Marten,
Expand Down Expand Up @@ -762,6 +771,7 @@ Read also more on the **Event Sourcing** and **CQRS** topics in my [blog](https:
- 📝 [How to slice the codebase effectively?](https://event-driven.io/en/how_to_slice_the_codebase_effectively/?utm_source=event_sourcing_net)
- 📝 [Generic does not mean Simple?](https://event-driven.io/en/generic_does_not_mean_simple/?utm_source=event_sourcing_net)
- 📝 [Can command return a value?](https://event-driven.io/en/can_command_return_a_value/?utm_source=event_sourcing_net)
- 📝 [CQRS is simpler than you think with .NET 6 and C# 10](https://event-driven.io/en/cqrs_is_simpler_than_you_think_with_net6/?utm_source=event_sourcing_net)
- 📝 [How to register all CQRS handlers by convention](https://event-driven.io/en/how_to_register_all_mediatr_handlers_by_convention/?utm_source=event_sourcing_net)
- 📝 [How to use ETag header for optimistic concurrency](https://event-driven.io/en/how_to_use_etag_header_for_optimistic_concurrency/?utm_source=event_sourcing_net)
- 📝 [Dealing with Eventual Consistency and Idempotency in MongoDB projections](https://event-driven.io/en/dealing_with_eventual_consistency_and_idempotency_in_mongodb_projections/?utm_source=event_sourcing_net)
Expand All @@ -770,7 +780,8 @@ Read also more on the **Event Sourcing** and **CQRS** topics in my [blog](https:
- 📝 [How to do snapshots in Marten?](https://event-driven.io/en/how_to_do_snapshots_in_Marten/?utm_source=event_sourcing_net)
- 📝 [Integrating Marten with other systems](https://event-driven.io/en/integrating_Marten/?utm_source=event_sourcing_net)
- 📝 [How to (not) do the events versioning?](https://event-driven.io/en/how_to_do_event_versioning/?utm_source=event_sourcing_net)
- 📝 [Simple patterns for events schema versioning](https://event-driven.io/en/simple_events_versioning_patterns/?utm_source=event_sourcing_net)
- 📝 [Simple patterns for events schema versioning](https://event-driven.io/en/simple_events_versioning_patterns/?utm_source=event_sourcing_net)
- 📝 [How to build a simple event pipeline](https://event-driven.io/en/how_to_build_simple_event_pipeline/?utm_source=event_sourcing_net)
- 📝 [How to create projections of events for nested object structures?](https://event-driven.io/en/how_to_create_projections_of_events_for_nested_object_structures/?utm_source=event_sourcing_net)
- 📝 [How to scale projections in the event-driven systems?](https://event-driven.io/en/how_to_scale_projections_in_the_event_driven_systems/?utm_source=event_sourcing_net)
- 📝 [Immutable Value Objects are simpler and more useful than you think!](https://event-driven.io/en/immutable_value_objects/?utm_source=event_sourcing_net)
Expand Down
1 change: 1 addition & 0 deletions Sample/EventPipelines/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Shows how to compose event handlers in the processing pipelines to:
- allows using interfaces and classes if you want to,
- can be used with Dependency Injection, but also without through builder,
- integrates with MediatR if you want to.
- 📝 Read more [How to build a simple event pipeline](https://event-driven.io/en/how_to_build_simple_event_pipeline/?utm_source=event_sourcing_net)

## Overview

Expand Down
7 changes: 7 additions & 0 deletions Sample/Warehouse.MinimalAPI/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Warehouse Minimal API
- CQRS flow using Minimal API,
- example how to inject handlers in MediatR like style to decouple API from handling,
- example of how and where to use C# Records, Nullable Reference Types, etc,
- No Event Sourcing! Using Entity Framework to show that CQRS is not bounded to Event Sourcing or any type of storage,
- No Aggregates! CQRS do not need DDD. Business logic can be handled in handlers.
- 📝 Read more [CQRS is simpler than you think with .NET 6 and C# 10](https://event-driven.io/en/cqrs_is_simpler_than_you_think_with_net6/?utm_source=event_sourcing_net)
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

namespace Warehouse.Api.Core.Commands;

public interface ICommandHandler<in T>
{
ValueTask Handle(T command, CancellationToken token);
}

public delegate ValueTask CommandHandler<in T>(T query, CancellationToken ct);

public static class CommandHandlerConfiguration
{
public static IServiceCollection AddCommandHandler<T, TCommandHandler>(
this IServiceCollection services,
Func<IServiceProvider, TCommandHandler>? configure = null
) where TCommandHandler : class, ICommandHandler<T>
{
if (configure == null)
{
services.AddTransient<TCommandHandler, TCommandHandler>();
services.AddTransient<ICommandHandler<T>, TCommandHandler>();
}
else
{
services.AddTransient<TCommandHandler, TCommandHandler>(configure);
services.AddTransient<ICommandHandler<T>, TCommandHandler>(configure);
}

services
.AddTransient<Func<T, CancellationToken, ValueTask>>(
sp => sp.GetRequiredService<ICommandHandler<T>>().Handle
)
.AddTransient<CommandHandler<T>>(
sp => sp.GetRequiredService<ICommandHandler<T>>().Handle
);

return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace Warehouse.Api.Core.Entities;

public static class EntitiesExtensions
{
public static async ValueTask AddAndSave<T>(this DbContext dbContext, T entity, CancellationToken ct)
where T : notnull
{
await dbContext.AddAsync(entity, ct);
await dbContext.SaveChangesAsync(ct);
}

public static ValueTask<T?> Find<T, TId>(this DbContext dbContext, TId id, CancellationToken ct)
where T : class where TId : notnull
=> dbContext.FindAsync<T>(new object[] {id}, ct);

public static IServiceCollection AddQueryable<T, TDbContext>(this IServiceCollection services)
where TDbContext : DbContext
where T : class =>
services.AddTransient(sp => sp.GetRequiredService<TDbContext>().Set<T>().AsNoTracking());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;

namespace Warehouse.Api.Core.Primitives;

internal static class MappingExtensions
{
public static T AssertNotNull<T>(this T? value, string? paramName = null)
where T : struct
{
if (value == null)
throw new ArgumentNullException(paramName);

return (T)value;
}

public static string AssertNotEmpty(this string? value, string? paramName = null)
=> !string.IsNullOrWhiteSpace(value) ? value : throw new ArgumentOutOfRangeException(paramName);

public static T AssertNotEmpty<T>(this T value, string? paramName = null)
where T : struct
=> AssertNotEmpty((T?)value, paramName);

public static T AssertNotEmpty<T>(this T? value, string? paramName = null)
where T : struct
{
var notNullValue = value.AssertNotNull(paramName);

if(Equals(notNullValue, default(T)))
throw new ArgumentOutOfRangeException(paramName);

return notNullValue;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

namespace Warehouse.Api.Core.Queries;

public interface IQueryHandler<in T, TResult>
{
ValueTask<TResult> Handle(T query, CancellationToken ct);
}

public delegate ValueTask<TResult> QueryHandler<in T, TResult>(T query, CancellationToken ct);

public static class QueryHandlerConfiguration
{
public static IServiceCollection AddQueryHandler<T, TResult, TQueryHandler>(
this IServiceCollection services,
Func<IServiceProvider, TQueryHandler>? configure = null
) where TQueryHandler : class, IQueryHandler<T, TResult>
{
if (configure == null)
{
services
.AddTransient<TQueryHandler, TQueryHandler>()
.AddTransient<IQueryHandler<T, TResult>, TQueryHandler>();
}
else
{
services
.AddTransient<TQueryHandler, TQueryHandler>(configure)
.AddTransient<IQueryHandler<T, TResult>, TQueryHandler>(configure);
}

services
.AddTransient<Func<T, CancellationToken, ValueTask<TResult>>>(
sp => sp.GetRequiredService<IQueryHandler<T, TResult>>().Handle
)
.AddTransient<QueryHandler<T, TResult>>(
sp => sp.GetRequiredService<IQueryHandler<T, TResult>>().Handle
);

return services;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace Warehouse.Api.Migrations
{
public partial class Initial : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Product",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Sku_Value = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Description = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Product", x => x.Id);
});
}

protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Product");
}
}
}
Loading