diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 2ffcc102c..d64bee45b 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -237,7 +237,14 @@ const config: UserConfig = { {text: 'Sql Server Integration', link: '/guide/durability/sqlserver'}, {text: 'PostgreSQL Integration', link: '/guide/durability/postgresql'}, {text: 'RavenDb Integration', link: '/guide/durability/ravendb'}, - {text: 'Entity Framework Core Integration', link: '/guide/durability/efcore'}, + {text: 'Entity Framework Core Integration', collapsed: false, link: '/guide/durability/efcore', items: [ + {text: 'Transactional Middleware', link: '/guide/durability/efcore/transactional-middleware'}, + {text: 'Transactional Inbox and Outbox', link: '/guide/durability/efcore/outbox-and-inbox'}, + {text: 'Operation Side Effects', link: '/guide/durability/efcore/operations'}, + {text: 'Saga Storage', link: '/guide/durability/efcore/sagas'}, + {text: 'Multi-Tenancy', link: '/guide/durability/efcore/multi-tenancy'} + + ]}, {text: 'Managing Message Storage', link: '/guide/durability/managing'}, {text: 'Dead Letter Storage', link: '/guide/durability/dead-letter-storage'}, {text: 'Idempotent Message Delivery', link:'/guide/durability/idempotency'} diff --git a/docs/guide/configuration.md b/docs/guide/configuration.md index e97996e59..c02d71bdd 100644 --- a/docs/guide/configuration.md +++ b/docs/guide/configuration.md @@ -145,8 +145,6 @@ builder.UseWolverine(opts => x.UseSqlServer(connectionString); }); - opts.UseEntityFrameworkCoreTransactions(); - // Add the auto transaction middleware attachment policy opts.Policies.AutoApplyTransactions(); }); @@ -154,7 +152,7 @@ builder.UseWolverine(opts => using var host = builder.Build(); await host.StartAsync(); ``` -snippet source | anchor +snippet source | anchor And lastly, you can just use `IServiceCollection.AddWolverine()` by itself. diff --git a/docs/guide/durability/dead-letter-storage.md b/docs/guide/durability/dead-letter-storage.md index a68d6fb9f..ca67288b1 100644 --- a/docs/guide/durability/dead-letter-storage.md +++ b/docs/guide/durability/dead-letter-storage.md @@ -82,7 +82,7 @@ app.MapDeadLettersEndpoints() ; ``` -snippet source | anchor +snippet source | anchor ### Using the Dead Letters REST API diff --git a/docs/guide/durability/efcore.md b/docs/guide/durability/efcore.md deleted file mode 100644 index 9de5e2498..000000000 --- a/docs/guide/durability/efcore.md +++ /dev/null @@ -1,310 +0,0 @@ -# Entity Framework Core Integration - -Wolverine supports [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/) through the `WolverineFx.EntityFrameworkCore` Nuget. -There's only a handful of touch points to EF Core that you need to be aware of: - -* Transactional middleware - Wolverine will both call `DbContext.SaveChangesAsync()` and flush any persisted messages for you -* EF Core as a saga storage mechanism - As long as one of your registered `DbContext` services has a mapping for the stateful saga type -* Outbox integration - Wolverine can use directly use a `DbContext` that has mappings for the Wolverine durable messaging, or at least use the database connection and current database transaction from a `DbContext` as part of durable, outbox message persistence. - - -## Registering Transactional Middleware and Saga Support - -Support for using Wolverine transactional middleware requires an explicit registration on `WolverineOptions` -shown below (it's an extension method): - - - -```cs -builder.Host.UseWolverine(opts => -{ - // Setting up Sql Server-backed message storage - // This requires a reference to Wolverine.SqlServer - opts.PersistMessagesWithSqlServer(connectionString, "wolverine"); - - // Set up Entity Framework Core as the support - // for Wolverine's transactional middleware - opts.UseEntityFrameworkCoreTransactions(); - - // Enrolling all local queues into the - // durable inbox/outbox processing - opts.Policies.UseDurableLocalQueues(); -}); -``` -snippet source | anchor - - -::: tip -When using the opt in `Handlers.AutoApplyTransactions()` option, Wolverine (really Lamar) can detect that your handler method uses a `DbContext` if it's a method argument, -a dependency of any service injected as a method argument, or a dependency of any service injected as a constructor -argument of the handler class. -::: - -That will enroll EF Core as both a strategy for stateful saga support and for transactional middleware. With this -option added, Wolverine will wrap transactional middleware around any message handler that has a dependency on any -type of `DbContext` like this one: - - - -```cs -[Transactional] -public static ItemCreated Handle( - // This would be the message - CreateItemCommand command, - - // Any other arguments are assumed - // to be service dependencies - ItemsDbContext db) -{ - // Create a new Item entity - var item = new Item - { - Name = command.Name - }; - - // Add the item to the current - // DbContext unit of work - db.Items.Add(item); - - // This event being returned - // by the handler will be automatically sent - // out as a "cascading" message - return new ItemCreated - { - Id = item.Id - }; -} -``` -snippet source | anchor - - -When using the transactional middleware around a message handler, the `DbContext` is used to persist -the outgoing messages as part of Wolverine's outbox support. - -## Auto Apply Transactional Middleware - -You can opt into automatically applying the transactional middleware to any handler that depends on a `DbContext` type -with the `AutoApplyTransactions()` option as shown below: - - - -```cs -var builder = Host.CreateApplicationBuilder(); -builder.UseWolverine(opts => -{ - var connectionString = builder.Configuration.GetConnectionString("database"); - - opts.Services.AddDbContextWithWolverineIntegration(x => - { - x.UseSqlServer(connectionString); - }); - - opts.UseEntityFrameworkCoreTransactions(); - - // Add the auto transaction middleware attachment policy - opts.Policies.AutoApplyTransactions(); -}); - -using var host = builder.Build(); -await host.StartAsync(); -``` -snippet source | anchor - - -With this option, you will no longer need to decorate handler methods with the `[Transactional]` attribute. - - -## Optimized DbContext Registration - -Wolverine can make a few performance optimizations for the `DbContext` integration with Wolverine if you use -this syntax for the service registration: - - - -```cs -// If you're okay with this, this will register the DbContext as normally, -// but make some Wolverine specific optimizations at the same time -builder.Services.AddDbContextWithWolverineIntegration( - x => x.UseSqlServer(connectionString), "wolverine"); -``` -snippet source | anchor - - -That registration will: - -1. Add mappings to your `DbContext` for persisted Wolverine messaging to make the outbox integration a little more efficient by - allowing Wolverine to utilize the command batching from EF Core for the message storage -2. Sets the `optionsLifetime` to `Singleton` scoped. This allows Wolverine to optimize the construction of your `DbContext` - objects at runtime when the configuration options do not vary by scope -3. Automatically registers the EF Core support for Wolverine transactional middleware and stateful saga support - -The AddDbContextWithWolverineIntegration has an additional last default parameter wolverineDatabaseSchema. It lets you control the name of a database schema where -Wolverine database table will be placed. The default value is null and creates Wolverine tables in the default schema. - -If you want to place Wolverine tables in a different schema you have to do the following: -1. Use `AddDbContextWithWolverineIntegration()` passing schema name as the last parameter. -2. Use the same schema name as a last parameter in call to `PersistMessagesWithSqlServer()` or `PersistMessagesWithPostgresql()` - -## Manually adding Envelope Mapping - -If not using the `AddDbContextWithWolverineIntegration()` extension method to register a `DbContext` in your system, you -can still explicitly add the Wolverine persistent message mapping into your `DbContext` with this call: - - - -```cs -public class SampleMappedDbContext : DbContext -{ - public SampleMappedDbContext(DbContextOptions options) : base(options) - { - } - - public DbSet Items { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - // This enables your DbContext to map the incoming and - // outgoing messages as part of the outbox - modelBuilder.MapWolverineEnvelopeStorage(); - - // Your normal EF Core mapping - modelBuilder.Entity(map => - { - map.ToTable("items", "mt_items"); - map.HasKey(x => x.Id); - map.Property(x => x.Name); - }); - } -} -``` -snippet source | anchor - -```cs -public class SampleMappedDbContext : DbContext -{ - public SampleMappedDbContext(DbContextOptions options) : base(options) - { - } - - public DbSet Items { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - // This enables your DbContext to map the incoming and - // outgoing messages as part of the outbox - modelBuilder.MapWolverineEnvelopeStorage(); - - // Your normal EF Core mapping - modelBuilder.Entity(map => - { - map.ToTable("items"); - map.HasKey(x => x.Id); - map.Property(x => x.Name); - }); - } -} -``` -snippet source | anchor - - - -## Outbox Outside of Wolverine Handlers - -::: tip -In all cases, the `IDbContextOutbox` services expose all the normal `IMessageBus` API. -::: - -To use EF Core with the Wolverine outbox outside of a Wolverine message handler, you have a couple options. - -First, you can use the `IDbContextOutbox` service where the `T` is your `DbContext` type as shown below: - - - -```cs -[HttpPost("/items/create2")] -public async Task Post( - [FromBody] CreateItemCommand command, - [FromServices] IDbContextOutbox outbox) -{ - // Create a new Item entity - var item = new Item - { - Name = command.Name - }; - - // Add the item to the current - // DbContext unit of work - outbox.DbContext.Items.Add(item); - - // Publish a message to take action on the new item - // in a background thread - await outbox.PublishAsync(new ItemCreated - { - Id = item.Id - }); - - // Commit all changes and flush persisted messages - // to the persistent outbox - // in the correct order - await outbox.SaveChangesAndFlushMessagesAsync(); -} -``` -snippet source | anchor - - -Or use the `IDbContextOutbox` as shown below, but in this case you will need to explicitly call `Enroll()` on -the `IDbContextOutbox` to connect the outbox sending to the `DbContext`: - - - -```cs -[HttpPost("/items/create3")] -public async Task Post3( - [FromBody] CreateItemCommand command, - [FromServices] ItemsDbContext dbContext, - [FromServices] IDbContextOutbox outbox) -{ - // Create a new Item entity - var item = new Item - { - Name = command.Name - }; - - // Add the item to the current - // DbContext unit of work - dbContext.Items.Add(item); - - // Gotta attach the DbContext to the outbox - // BEFORE sending any messages - outbox.Enroll(dbContext); - - // Publish a message to take action on the new item - // in a background thread - await outbox.PublishAsync(new ItemCreated - { - Id = item.Id - }); - - // Commit all changes and flush persisted messages - // to the persistent outbox - // in the correct order - await outbox.SaveChangesAndFlushMessagesAsync(); -} -``` -snippet source | anchor - - -## As Saga Storage - -There's actually nothing to do other than to make a mapping of the `Saga` subclass that's your stateful saga inside -a registered `DbContext`. - -## Storage Side Effects - -This integration includes full support for the [storage action side effects](/guide/handlers/side-effects.html#storage-side-effects) -model when using EF Core with Wolverine. The only exception is that the `Store` "upsert" operation from the -side effects translates to an EF Core `DbContext` update. - -## Entity Attribute Loading - -The EF Core integration is able to completely support the [Entity attribute usage](/guide/handlers/persistence.html#automatically-loading-entities-to-method-parameters). diff --git a/docs/guide/durability/efcore/index.md b/docs/guide/durability/efcore/index.md new file mode 100644 index 000000000..4ee19c0a4 --- /dev/null +++ b/docs/guide/durability/efcore/index.md @@ -0,0 +1,90 @@ +# Entity Framework Core Integration + +Wolverine supports [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/) through the `WolverineFx.EntityFrameworkCore` Nuget. + +* Transactional middleware - Wolverine will both call `DbContext.SaveChangesAsync()` and flush any persisted messages for you +* EF Core as a saga storage mechanism - As long as one of your registered `DbContext` services has a mapping for the stateful saga type +* Outbox integration - Wolverine can use directly use a `DbContext` that has mappings for the Wolverine durable messaging, or at least use the database connection and current database transaction from a `DbContext` as part of durable, outbox message persistence. +* [Multi-Tenancy with EF Core](./multi-tenancy) + +## Getting Started + +The first step is to just install the `WolverineFx.EntityFrameworkCore` Nuget: + +```bash +dotnet add package WolverineFx.EntityFrameworkCore +``` + +::: warning +For right now, it's perfectly possible to use multiple `DbContext` types with one Wolverine application and Wolverine +is perfectly capable of using the correct `DbContext` type for `Saga` types. **But**, Wolverine can only use the transactional +inbox/outbox with a single database registration. This limitation will be lifted later as folks are going to eventually hit +this limitation with modular monolith approaches. +::: + +With that in place, there's two basic things you need in order to fully use EF Core with Wolverine as shown below: + + + +```cs +var builder = Host.CreateApplicationBuilder(); + +var connectionString = builder.Configuration.GetConnectionString("sqlserver"); + +// Register a DbContext or multiple DbContext types as normal +builder.Services.AddDbContext( + x => x.UseSqlServer(connectionString), + + // This is actually a significant performance gain + // for Wolverine's sake + optionsLifetime:ServiceLifetime.Singleton); + +// Register Wolverine +builder.UseWolverine(opts => +{ + // You'll need to independently tell Wolverine where and how to + // store messages as part of the transactional inbox/outbox + opts.PersistMessagesWithSqlServer(connectionString); + + // Adding EF Core transactional middleware, saga support, + // and EF Core support for Wolverine storage operations + opts.UseEntityFrameworkCoreTransactions(); +}); + +// Rest of your bootstrapping... +``` +snippet source | anchor + + +Do note that I purposely configured the `ServiceLifetime` of the `DbContextOptions` for our `DbContext` type to be `Singleton`. +That actually makes a non-trivial performance optimization for Wolverine and how it can treat `DbContext` types at runtime. + +Or alternatively, you can do this in one step with this equivalent approach: + + + +```cs +var builder = Host.CreateApplicationBuilder(); + +var connectionString = builder.Configuration.GetConnectionString("sqlserver"); + +builder.UseWolverine(opts => +{ + // You'll need to independently tell Wolverine where and how to + // store messages as part of the transactional inbox/outbox + opts.PersistMessagesWithSqlServer(connectionString); + + // Registers the DbContext type in your IoC container, sets the DbContextOptions + // lifetime to "Singleton" to optimize Wolverine usage, and also makes sure that + // your Wolverine service has all the EF Core transactional middleware, saga support, + // and storage operation helpers activated for this application + opts.Services.AddDbContextWithWolverineIntegration( + x => x.UseSqlServer(connectionString)); +}); +``` +snippet source | anchor + + + + +Right now, we've tested Wolverine with EF Core using both [SQL Server](/guide/durability/sqlserver) and [PostgreSQL](/guide/durability/postgresql) persistence. diff --git a/docs/guide/durability/efcore/multi-tenancy.md b/docs/guide/durability/efcore/multi-tenancy.md new file mode 100644 index 000000000..cf06943fb --- /dev/null +++ b/docs/guide/durability/efcore/multi-tenancy.md @@ -0,0 +1,102 @@ +# Multi-Tenancy with EF Core + +Wolverine has first class support for using a single EF Core `DbContext` type that potentially uses different databases +for different clients within your system, and this includes every single bit of EF Core capabilities with Wolverine: + +* Wolverine will manage a separate transactional inbox & outbox for each tenant database and any main database +* The transactional middleware is multi-tenant aware for EF Core +* Wolverine's [Tenant id detection for HTTP](/guide/http/multi-tenancy.html#tenant-id-detection) is supported by the EF Core integration +* The [storage actions](/guide/durability/efcore/operations) and `[Entity]` attribute support for EF Core will respect the multi-tenancy + +Alright, let's get into a first concrete sample. In this simplest usage, I'm assuming that there are only three separate +tenant databases, and each database will only hold data for a single tenant. + +To use EF Core with [multi-tenanted PostgreSQL](/guide/durability/postgresql.html#multi-tenancy) storage, we can use this: + + + +```cs +var builder = Host.CreateApplicationBuilder(); + +var configuration = builder.Configuration; + +builder.UseWolverine(opts => +{ + // First, you do have to have a "main" PostgreSQL database for messaging persistence + // that will store information about running nodes, agents, and non-tenanted operations + opts.PersistMessagesWithPostgresql(configuration.GetConnectionString("main")) + + // Add known tenants at bootstrapping time + .RegisterStaticTenants(tenants => + { + // Add connection strings for the expected tenant ids + tenants.Register("tenant1", configuration.GetConnectionString("tenant1")); + tenants.Register("tenant2", configuration.GetConnectionString("tenant2")); + tenants.Register("tenant3", configuration.GetConnectionString("tenant3")); + }); + + opts.Services.AddDbContextWithWolverineManagedMultiTenancy((builder, connectionString, _) => + { + builder.UseNpgsql(connectionString.Value, b => b.MigrationsAssembly("MultiTenantedEfCoreWithPostgreSQL")); + }, AutoCreate.CreateOrUpdate); +}); +``` +snippet source | anchor + + +And instead with [multi-tenanted SQL Server](/guide/durability/sqlserver.html#multi-tenancy) storage: + + + +```cs +var builder = Host.CreateApplicationBuilder(); + +var configuration = builder.Configuration; + +builder.UseWolverine(opts => +{ + // First, you do have to have a "main" PostgreSQL database for messaging persistence + // that will store information about running nodes, agents, and non-tenanted operations + opts.PersistMessagesWithSqlServer(configuration.GetConnectionString("main")) + + // Add known tenants at bootstrapping time + .RegisterStaticTenants(tenants => + { + // Add connection strings for the expected tenant ids + tenants.Register("tenant1", configuration.GetConnectionString("tenant1")); + tenants.Register("tenant2", configuration.GetConnectionString("tenant2")); + tenants.Register("tenant3", configuration.GetConnectionString("tenant3")); + }); + + // Just to show that you *can* use more than one DbContext + opts.Services.AddDbContextWithWolverineManagedMultiTenancy((builder, connectionString, _) => + { + // You might have to set the migration assembly + builder.UseSqlServer(connectionString.Value, b => b.MigrationsAssembly("MultiTenantedEfCoreWithSqlServer")); + }, AutoCreate.CreateOrUpdate); + + opts.Services.AddDbContextWithWolverineManagedMultiTenancy((builder, connectionString, _) => + { + builder.UseSqlServer(connectionString.Value, b => b.MigrationsAssembly("MultiTenantedEfCoreWithSqlServer")); + }, AutoCreate.CreateOrUpdate); +}); +``` +snippet source | anchor + + +Note in both samples how I'm registering the `DbContext` types. There's a fluent interface first to register the multi-tenanted +database storage, then a call to register a `DbContext` with multi-tenancy. You'll have to supply Wolverine with a lambda +to configure the `DbContextOptionsBuilder` for the individual `DbContext` object. At runtime, Wolverine will be passing in the right +connection string for the active tenant id. There is also other overloads to configure based on a `DbDataSource` if using +PostgreSQL or to also take in a `TenantId` value type that will give you the active tenant id if you need to use that +for setting EF Core query filters like [this example from the Microsoft documentation](https://learn.microsoft.com/en-us/ef/core/miscellaneous/multitenancy#an-example-solution-single-database). + +## Combine with Marten + +It's perfectly possible to use [Marten](https://martendb.io) and its multi-tenancy support for targeting a separate database +with EF Core using the same databases. Maybe you're using Marten for event sourcing, then using EF Core for flat table projections. +Regardless, you simply allow Marten to manage the multi-tenancy and the relationship between tenant ids and the various databases, +and the Wolverine EF Core integration can more or less ride on Marten's coat tails: + +snippet: sample_use_multi_tenancy_with_both_marten_and_ef_core + diff --git a/docs/guide/durability/efcore/operations.md b/docs/guide/durability/efcore/operations.md new file mode 100644 index 000000000..92ff00253 --- /dev/null +++ b/docs/guide/durability/efcore/operations.md @@ -0,0 +1,144 @@ +# Storage Operations with EF Core + +Just know that Wolverine completely supports the concept of [Storage Operations](/guide/handlers/side-effects.html#storage-side-effects) for EF Core. + +Assuming you have an EF Core `DbContext` type like this registered in your system: + + + +```cs +public class TodoDbContext : DbContext +{ + public TodoDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Todos { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(map => + { + map.ToTable("todos", "todo_app"); + map.HasKey(x => x.Id); + map.Property(x => x.Name); + map.Property(x => x.IsComplete).HasColumnName("is_complete"); + }); + } +} +``` +snippet source | anchor + + +You can use storage operations in Wolverine message handlers or HTTP endpoints like these samples from the Wolverine +test suite: + + + +```cs +public static class TodoHandler +{ + public static Insert Handle(CreateTodo command) => Storage.Insert(new Todo + { + Id = command.Id, + Name = command.Name + }); + + public static Store Handle(CreateTodo2 command) => Storage.Store(new Todo + { + Id = command.Id, + Name = command.Name + }); + + // Use "Id" as the default member + public static Update Handle( + // The first argument is always the incoming message + RenameTodo command, + + // By using this attribute, we're telling Wolverine + // to load the Todo entity from the configured + // persistence of the app using a member on the + // incoming message type + [Entity] Todo todo) + { + // Do your actual business logic + todo.Name = command.Name; + + // Tell Wolverine that you want this entity + // updated in persistence + return Storage.Update(todo); + } + + // Use "TodoId" as the default member + public static Update Handle(RenameTodo2 command, [Entity] Todo todo) + { + todo.Name = command.Name; + return Storage.Update(todo); + } + + // Use the explicit member + public static Update Handle(RenameTodo3 command, [Entity("Identity")] Todo todo) + { + todo.Name = command.Name; + return Storage.Update(todo); + } + + public static Delete Handle(DeleteTodo command, [Entity("Identity")] Todo todo) + { + return Storage.Delete(todo); + } + + public static IStorageAction Handle(AlterTodo command, [Entity("Identity")] Todo todo) + { + switch (command.Action) + { + case StorageAction.Delete: + return Storage.Delete(todo); + case StorageAction.Update: + todo.Name = command.Name; + return Storage.Update(todo); + case StorageAction.Store: + todo.Name = command.Name; + return Storage.Store(todo); + default: + return Storage.Nothing(); + } + } + + public static IStorageAction Handle(MaybeInsertTodo command) + { + if (command.ShouldInsert) + { + return Storage.Insert(new Todo { Id = command.Id, Name = command.Name }); + } + + return Storage.Nothing(); + } + + public static Insert? Handle(ReturnNullInsert command) => null; + + public static IStorageAction? Handle(ReturnNullStorageAction command) => null; + + public static IStorageAction Handle(CompleteTodo command, [Entity] Todo todo) + { + if (todo == null) throw new ArgumentNullException(nameof(todo)); + todo.IsComplete = true; + return Storage.Update(todo); + } + + public static IStorageAction Handle(MaybeCompleteTodo command, [Entity(Required = false)] Todo? todo) + { + if (todo == null) return Storage.Nothing(); + todo.IsComplete = true; + return Storage.Update(todo); + } +} +``` +snippet source | anchor + + +## [Entity] + +Wolverine also supports the usage of the `[Entity]` attribute to load entity data by its identity with EF Core. As you'd +expect, Wolverine can "find" the right EF Core `DbContext` type for the entity type through IoC service registrations. + diff --git a/docs/guide/durability/efcore/outbox-and-inbox.md b/docs/guide/durability/efcore/outbox-and-inbox.md new file mode 100644 index 000000000..374cc6de7 --- /dev/null +++ b/docs/guide/durability/efcore/outbox-and-inbox.md @@ -0,0 +1,173 @@ +# Transactional Inbox and Outbox with EF Core + +Wolverine is able to integrate with EF Core inside of its transactional middleware in either message handlers or HTTP +endpoints to apply the [transactional inbox and outbox mechanics](/guide/durability/) for outgoing messages (local messages actually go straight to the inbox). + +::: tip +Database round trips, or really any network round trips, are a frequent cause of poor system performance. Wolverine and other +Critter Stack tools try to take this into account in its internals. With the EF Core integration, you might need to do just a little +bit to help Wolverine out with mapping envelope types to take advantage of database query batching. +::: + +You can optimize this by adding mappings for Wolverine's envelope storage to your `DbContext` types such that Wolverine can +just use EF Core to persist new messages and depend on EF Core database command batching. Otherwise Wolverine has to use +the exposed database `DbConnection` off of the active `DbContext` and make completely separate calls to the database (but at least +in the same transaction!) to persist new messages at the same time it's calling `DbContext.SaveChangesAsync()` with any +pending entity changes. + +You can help Wolverine out by either using the manual envelope mapping explained next, or registering your `DbContext` +with the `AddDbContextWithWolverineIntegration()` option that quietly adds the Wolverine envelope storage mapping +to that `DbContext` for you. + +## Manually adding Envelope Mapping + +If not using the `AddDbContextWithWolverineIntegration()` extension method to register a `DbContext` in your system, you +can still explicitly add the Wolverine persistent message mapping into your `DbContext` with this call: + + + +```cs +public class SampleMappedDbContext : DbContext +{ + public SampleMappedDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Items { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // This enables your DbContext to map the incoming and + // outgoing messages as part of the outbox + modelBuilder.MapWolverineEnvelopeStorage(); + + // Your normal EF Core mapping + modelBuilder.Entity(map => + { + map.ToTable("items", "mt_items"); + map.HasKey(x => x.Id); + map.Property(x => x.Name); + }); + } +} +``` +snippet source | anchor + +```cs +public class SampleMappedDbContext : DbContext +{ + public SampleMappedDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Items { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // This enables your DbContext to map the incoming and + // outgoing messages as part of the outbox + modelBuilder.MapWolverineEnvelopeStorage(); + + // Your normal EF Core mapping + modelBuilder.Entity(map => + { + map.ToTable("items"); + map.HasKey(x => x.Id); + map.Property(x => x.Name); + }); + } +} +``` +snippet source | anchor + + +## Outbox Outside of Wolverine Handlers + +::: warning +Honestly, we had to do this feature, but it's just always going to be easiest to use Wolverine HTTP handlers or message handlers +for the EF Core + transactional outbox support. +::: + +::: tip +In all cases, the `IDbContextOutbox` services expose all the normal `IMessageBus` API. +::: + +To use EF Core with the Wolverine outbox outside of a Wolverine message handler (maybe inside an ASP.Net MVC Core `Controller`, or within Minimal API maybe?), you have a couple options. + +First, you can use the `IDbContextOutbox` service where the `T` is your `DbContext` type as shown below: + + + +```cs +[HttpPost("/items/create2")] +public async Task Post( + [FromBody] CreateItemCommand command, + [FromServices] IDbContextOutbox outbox) +{ + // Create a new Item entity + var item = new Item + { + Name = command.Name + }; + + // Add the item to the current + // DbContext unit of work + outbox.DbContext.Items.Add(item); + + // Publish a message to take action on the new item + // in a background thread + await outbox.PublishAsync(new ItemCreated + { + Id = item.Id + }); + + // Commit all changes and flush persisted messages + // to the persistent outbox + // in the correct order + await outbox.SaveChangesAndFlushMessagesAsync(); +} +``` +snippet source | anchor + + +Or use the `IDbContextOutbox` as shown below, but in this case you will need to explicitly call `Enroll()` on +the `IDbContextOutbox` to connect the outbox sending to the `DbContext`: + + + +```cs +[HttpPost("/items/create3")] +public async Task Post3( + [FromBody] CreateItemCommand command, + [FromServices] ItemsDbContext dbContext, + [FromServices] IDbContextOutbox outbox) +{ + // Create a new Item entity + var item = new Item + { + Name = command.Name + }; + + // Add the item to the current + // DbContext unit of work + dbContext.Items.Add(item); + + // Gotta attach the DbContext to the outbox + // BEFORE sending any messages + outbox.Enroll(dbContext); + + // Publish a message to take action on the new item + // in a background thread + await outbox.PublishAsync(new ItemCreated + { + Id = item.Id + }); + + // Commit all changes and flush persisted messages + // to the persistent outbox + // in the correct order + await outbox.SaveChangesAndFlushMessagesAsync(); +} +``` +snippet source | anchor + diff --git a/docs/guide/durability/efcore/sagas.md b/docs/guide/durability/efcore/sagas.md new file mode 100644 index 000000000..e0e4af151 --- /dev/null +++ b/docs/guide/durability/efcore/sagas.md @@ -0,0 +1,138 @@ +# Saga Storage + +Wolverine can use registered EF Core `DbContext` types for [saga persistence](/guide/durability) as long as the EF Core transactional support +is added to the application. There's absolutely nothing you need to do to enable this except for having a mapping for +whatever `Saga` type you need to persist in a registered `DbContext` type. As long as your `DbContext` +with a mapping for a particular `Saga` type is registered in the IoC container for your application +and Wolverine's EF Core transactional support is active, Wolverine will be able to find and use +the correct `DbContext` type for your `Saga` at runtime. + +You do *not* need to use the `WolverineOptions.AddSagaType()` option with EF Core saga, that option +is strictly for the [lightweight saga storage](/guide/durability/sagas.html#lightweight-saga-storage) with SQL Server or PostgreSQL. + +To make that concrete, let's say you've got a simplistic `Order` saga type like this: + + + +```cs +public enum OrderStatus +{ + Pending = 0, + CreditReserved = 1, + CreditLimitExceeded = 2, + Approved = 3, + Rejected = 4 +} + +public class Order : Saga +{ + public string? Id { get; set; } + public OrderStatus OrderStatus { get; set; } = OrderStatus.Pending; + + public object[] Start( + OrderPlaced orderPlaced, + ILogger logger + ) + { + Id = orderPlaced.OrderId; + logger.LogInformation("Order {OrderId} placed", Id); + OrderStatus = OrderStatus.Pending; + return + [ + new ReserveCredit( + orderPlaced.OrderId, + orderPlaced.CustomerId, + orderPlaced.Amount + ) + ]; + } + + public object[] Handle( + CreditReserved creditReserved, + ILogger logger + ) + { + OrderStatus = OrderStatus.CreditReserved; + logger.LogInformation("Credit reserver for Order {OrderId}", Id); + return [new ApproveOrder(creditReserved.OrderId, creditReserved.CustomerId)]; + } + + public void Handle( + OrderApproved orderApproved, + ILogger logger + ) + { + OrderStatus = OrderStatus.Approved; + logger.LogInformation("Order {OrderId} approved", Id); + } + + public object[] Handle( + CreditLimitExceeded creditLimitExceeded, + ILogger logger + ) + { + OrderStatus = OrderStatus.CreditLimitExceeded; + return [new RejectOrder(creditLimitExceeded.OrderId)]; + } + + public void Handle( + OrderRejected orderRejected, + ILogger logger + ) + { + OrderStatus = OrderStatus.Rejected; + logger.LogInformation("Order {OrderId} rejected", Id); + MarkCompleted(); + } +} +``` +snippet source | anchor + + +And a matching `OrdersDbContext` that can persist that type like so: + + + +```cs +public class OrdersDbContext : DbContext +{ + protected OrdersDbContext() + { + } + + public OrdersDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Orders { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Your normal EF Core mapping + modelBuilder.Entity(map => + { + map.ToTable("orders", "sample"); + map.HasKey(x => x.Id); + map.Property(x => x.OrderStatus) + .HasConversion(v => v.ToString(), v => Enum.Parse(v)); + }); + } +} +``` +snippet source | anchor + + +There's no other registration to do other than adding the `OrdersDbContext` to your IoC container and enabling +the Wolverine EF Core middleware as shown in the [getting started with EF Core](/guide/durability/efcore/#getting-started) section. + +## When to Use EF Core vs Lightweight Storage? + +As to the question, when should you opt for lightweight storage where Wolverine just sticks serialized JSON into a single +field for a saga versus using fullblown EF Core mapping? If you have any need to *also* persist other data with a `DbContext` +service while executing any of the `Saga` steps, use EF Core mapping with that same `DbContext` type so that Wolverine can +easily manage the changes in one single transaction. If you prefer having a flat table, maybe just because it'll be easier +to monitor through normal database tooling, use EF Core. If you just want to go fast and don't want to mess with ORM mapping, +then use the lightweight storage with Wolverine. + +Do note that using `AddSagaType()` for a `Saga` type will win out over any EF Core mappings and Wolverine will try to +use the lightweight storage in that case. diff --git a/docs/guide/durability/efcore/transactional-middleware.md b/docs/guide/durability/efcore/transactional-middleware.md new file mode 100644 index 000000000..a38417d50 --- /dev/null +++ b/docs/guide/durability/efcore/transactional-middleware.md @@ -0,0 +1,103 @@ +# Transactional Middleware + +Support for using Wolverine transactional middleware requires an explicit registration on `WolverineOptions` +shown below (it's an extension method): + + + +```cs +builder.Host.UseWolverine(opts => +{ + // Setting up Sql Server-backed message storage + // This requires a reference to Wolverine.SqlServer + opts.PersistMessagesWithSqlServer(connectionString, "wolverine"); + + // Set up Entity Framework Core as the support + // for Wolverine's transactional middleware + opts.UseEntityFrameworkCoreTransactions(); + + // Enrolling all local queues into the + // durable inbox/outbox processing + opts.Policies.UseDurableLocalQueues(); +}); +``` +snippet source | anchor + + +::: tip +When using the opt in `Handlers.AutoApplyTransactions()` option, Wolverine (really Lamar) can detect that your handler method uses a `DbContext` if it's a method argument, +a dependency of any service injected as a method argument, or a dependency of any service injected as a constructor +argument of the handler class. +::: + +That will enroll EF Core as both a strategy for stateful saga support and for transactional middleware. With this +option added, Wolverine will wrap transactional middleware around any message handler that has a dependency on any +type of `DbContext` like this one: + + + +```cs +[Transactional] +public static ItemCreated Handle( + // This would be the message + CreateItemCommand command, + + // Any other arguments are assumed + // to be service dependencies + ItemsDbContext db) +{ + // Create a new Item entity + var item = new Item + { + Name = command.Name + }; + + // Add the item to the current + // DbContext unit of work + db.Items.Add(item); + + // This event being returned + // by the handler will be automatically sent + // out as a "cascading" message + return new ItemCreated + { + Id = item.Id + }; +} +``` +snippet source | anchor + + +When using the transactional middleware around a message handler, the `DbContext` is used to persist +the outgoing messages as part of Wolverine's outbox support. + +## Auto Apply Transactional Middleware + +You can opt into automatically applying the transactional middleware to any handler that depends on a `DbContext` type +with the `AutoApplyTransactions()` option as shown below: + + + +```cs +var builder = Host.CreateApplicationBuilder(); +builder.UseWolverine(opts => +{ + var connectionString = builder.Configuration.GetConnectionString("database"); + + opts.Services.AddDbContextWithWolverineIntegration(x => + { + x.UseSqlServer(connectionString); + }); + + // Add the auto transaction middleware attachment policy + opts.Policies.AutoApplyTransactions(); +}); + +using var host = builder.Build(); +await host.StartAsync(); +``` +snippet source | anchor + + +With this option, you will no longer need to decorate handler methods with the `[Transactional]` attribute. + diff --git a/docs/guide/durability/postgresql.md b/docs/guide/durability/postgresql.md index 76192bf5f..706dc42ef 100644 --- a/docs/guide/durability/postgresql.md +++ b/docs/guide/durability/postgresql.md @@ -161,9 +161,14 @@ builder.UseWolverine(opts => tenants.Register("tenant2", configuration.GetConnectionString("tenant2")); tenants.Register("tenant3", configuration.GetConnectionString("tenant3")); }); + + opts.Services.AddDbContextWithWolverineManagedMultiTenancy((builder, connectionString, _) => + { + builder.UseNpgsql(connectionString.Value, b => b.MigrationsAssembly("MultiTenantedEfCoreWithPostgreSQL")); + }, AutoCreate.CreateOrUpdate); }); ``` -snippet source | anchor +snippet source | anchor Since the underlying [Npgsql library](https://www.npgsql.org/) supports the `DbDataSource` concept, and you might need to use this for a variety of reasons, you can also @@ -196,7 +201,7 @@ public class OurFancyPostgreSQLMultiTenancy : IWolverineExtension } } ``` -snippet source | anchor +snippet source | anchor And add that to the greater application like so: @@ -211,7 +216,7 @@ var host = Host.CreateDefaultBuilder() services.AddSingleton(); }).StartAsync(); ``` -snippet source | anchor +snippet source | anchor ::: warning @@ -247,9 +252,10 @@ builder.UseWolverine(opts => seed.Register("tenant2", configuration.GetConnectionString("tenant2")); seed.Register("tenant3", configuration.GetConnectionString("tenant3")); }); + }); ``` -snippet source | anchor +snippet source | anchor ::: info @@ -266,8 +272,8 @@ Here's some more important background on the multi-tenancy support: main database and all the tenant databases including schema migrations * Wolverine's transactional middleware is aware of the multi-tenancy and can connect to the correct database based on the `IMesageContext.TenantId` or utilize the tenant id detection in Wolverine.HTTP as well +* You can "plug in" a custom implementation of `ITenantSource` to manage tenant id to connection string assignments in whatever way works for your deployed system -MORE -- other usages, including custom ITenantSource ## Lightweight Saga Usage diff --git a/docs/guide/durability/sqlserver.md b/docs/guide/durability/sqlserver.md index 4a498de70..e38500d1c 100644 --- a/docs/guide/durability/sqlserver.md +++ b/docs/guide/durability/sqlserver.md @@ -182,9 +182,21 @@ builder.UseWolverine(opts => tenants.Register("tenant2", configuration.GetConnectionString("tenant2")); tenants.Register("tenant3", configuration.GetConnectionString("tenant3")); }); + + // Just to show that you *can* use more than one DbContext + opts.Services.AddDbContextWithWolverineManagedMultiTenancy((builder, connectionString, _) => + { + // You might have to set the migration assembly + builder.UseSqlServer(connectionString.Value, b => b.MigrationsAssembly("MultiTenantedEfCoreWithSqlServer")); + }, AutoCreate.CreateOrUpdate); + + opts.Services.AddDbContextWithWolverineManagedMultiTenancy((builder, connectionString, _) => + { + builder.UseSqlServer(connectionString.Value, b => b.MigrationsAssembly("MultiTenantedEfCoreWithSqlServer")); + }, AutoCreate.CreateOrUpdate); }); ``` -snippet source | anchor +snippet source | anchor ::: warning @@ -222,7 +234,7 @@ builder.UseWolverine(opts => }); }); ``` -snippet source | anchor +snippet source | anchor ::: info @@ -239,8 +251,8 @@ Here's some more important background on the multi-tenancy support: main database and all the tenant databases including schema migrations * Wolverine's transactional middleware is aware of the multi-tenancy and can connect to the correct database based on the `IMesageContext.TenantId` or utilize the tenant id detection in Wolverine.HTTP as well +* You can "plug in" a custom implementation of `ITenantSource` to manage tenant id to connection string assignments in whatever way works for your deployed system -MORE -- other usages, including custom ITenantSource diff --git a/docs/guide/extensions.md b/docs/guide/extensions.md index 7a50ec7de..db9f0cd76 100644 --- a/docs/guide/extensions.md +++ b/docs/guide/extensions.md @@ -242,7 +242,7 @@ var app = builder.Build(); // you will need to explicitly call this *before* MapWolverineEndpoints() await app.Services.ApplyAsyncWolverineExtensions(); ``` -snippet source | anchor +snippet source | anchor ## Wolverine Plugin Modules diff --git a/docs/guide/handlers/side-effects.md b/docs/guide/handlers/side-effects.md index 4d159fbb9..0732ca424 100644 --- a/docs/guide/handlers/side-effects.md +++ b/docs/guide/handlers/side-effects.md @@ -255,7 +255,7 @@ public static class StoreManyHandler } } ``` -snippet source | anchor +snippet source | anchor The `UnitOfWork` is really just a `List>` that can relay zero to many storage diff --git a/docs/guide/http/endpoints.md b/docs/guide/http/endpoints.md index 61cbee183..05f814a6a 100644 --- a/docs/guide/http/endpoints.md +++ b/docs/guide/http/endpoints.md @@ -174,7 +174,7 @@ public static OrderShipped Ship(ShipOrder command, Order order) return new OrderShipped(); } ``` -snippet source | anchor +snippet source | anchor ## JSON Handling @@ -319,7 +319,7 @@ and register that strategy within our `MapWolverineEndpoints()` set up like so: // Customizing parameter handling opts.AddParameterHandlingStrategy(); ``` -snippet source | anchor +snippet source | anchor And lastly, here's the application within an HTTP endpoint for extra context: diff --git a/docs/guide/http/fluentvalidation.md b/docs/guide/http/fluentvalidation.md index d3f071570..128698713 100644 --- a/docs/guide/http/fluentvalidation.md +++ b/docs/guide/http/fluentvalidation.md @@ -44,5 +44,5 @@ app.MapWolverineEndpoints(opts => // Wolverine.Http.FluentValidation opts.UseFluentValidationProblemDetailMiddleware(); ``` -snippet source | anchor +snippet source | anchor diff --git a/docs/guide/http/index.md b/docs/guide/http/index.md index a7caa2858..c7b198b32 100644 --- a/docs/guide/http/index.md +++ b/docs/guide/http/index.md @@ -192,7 +192,17 @@ works on this, the simple amelioration is to either "just" pre-generate the code Or, you can opt for `Eager` initialization of the HTTP endpoints to side step this problem in development when pre-generating types isn't viable: -snippet: sample_eager_http_warmup + + +```cs +var app = builder.Build(); + +app.MapWolverineEndpoints(x => x.WarmUpRoutes = RouteWarmup.Eager); + +return await app.RunJasperFxCommands(args); +``` +snippet source | anchor + diff --git a/docs/guide/http/integration.md b/docs/guide/http/integration.md index ab8824dce..7976f2311 100644 --- a/docs/guide/http/integration.md +++ b/docs/guide/http/integration.md @@ -120,7 +120,7 @@ public async Task hello_world() result.ReadAsText().ShouldBe("Hello."); } ``` -snippet source | anchor +snippet source | anchor Moving on to the actual `Todo` problem domain, let's assume we've got a class like this: diff --git a/docs/guide/http/marten.md b/docs/guide/http/marten.md index 14e4de6f9..d02592b92 100644 --- a/docs/guide/http/marten.md +++ b/docs/guide/http/marten.md @@ -124,7 +124,7 @@ public static OrderShipped Ship(ShipOrder2 command, [Aggregate] Order order) return new OrderShipped(); } ``` -snippet source | anchor +snippet source | anchor Using this version of the "aggregate workflow", you no longer have to supply a command in the request body, so you could @@ -143,7 +143,7 @@ public static OrderShipped Ship3([Aggregate] Order order) return new OrderShipped(); } ``` -snippet source | anchor +snippet source | anchor A couple other notes: @@ -236,7 +236,7 @@ public class Order public bool IsShipped() => Shipped.HasValue; } ``` -snippet source | anchor +snippet source | anchor To append a single event to an event stream from an HTTP endpoint, you can use a return value like so: @@ -255,7 +255,7 @@ public static OrderShipped Ship(ShipOrder command, Order order) return new OrderShipped(); } ``` -snippet source | anchor +snippet source | anchor Or potentially append multiple events using the `Events` type as a return value like this sample: @@ -291,7 +291,7 @@ public static (OrderStatus, Events) Post(MarkItemReady command, Order order) return (new OrderStatus(order.Id, order.IsReadyToShip()), events); } ``` -snippet source | anchor +snippet source | anchor ### Responding with the Updated Aggregate @@ -317,7 +317,7 @@ public static (UpdatedAggregate, Events) ConfirmDifferent(ConfirmOrder command, ); } ``` -snippet source | anchor +snippet source | anchor ## Reading the Latest Version of an Aggregate @@ -336,7 +336,7 @@ an HTTP endpoint method, use the `[ReadAggregate]` attribute like this: [WolverineGet("/orders/latest/{id}")] public static Order GetLatest(Guid id, [ReadAggregate] Order order) => order; ``` -snippet source | anchor +snippet source | anchor If the aggregate doesn't exist, the HTTP request will stop with a 404 status code. @@ -356,7 +356,7 @@ Register it in `WolverineHttpOptions` like this: ```cs opts.UseMartenCompiledQueryResultPolicy(); ``` -snippet source | anchor +snippet source | anchor If you now return a compiled query from an Endpoint the result will get directly streamed to the client as JSON. Short circuiting JSON deserialization. diff --git a/docs/guide/http/mediator.md b/docs/guide/http/mediator.md index 916cffcf4..03a7c9904 100644 --- a/docs/guide/http/mediator.md +++ b/docs/guide/http/mediator.md @@ -45,7 +45,7 @@ app.MapPostToWolverine("/wolverine/request"); app.MapDeleteToWolverine("/wolverine/request"); app.MapPutToWolverine("/wolverine/request"); ``` -snippet source | anchor +snippet source | anchor With this mechanism, Wolverine is able to optimize the runtime function for Minimal API by eliminating IoC service locations diff --git a/docs/guide/http/metadata.md b/docs/guide/http/metadata.md index c7453bca4..edb027456 100644 --- a/docs/guide/http/metadata.md +++ b/docs/guide/http/metadata.md @@ -97,7 +97,7 @@ builder.Services.AddSwaggerGen(x => x.OperationFilter(); }); ``` -snippet source | anchor +snippet source | anchor ## Operation Id diff --git a/docs/guide/http/middleware.md b/docs/guide/http/middleware.md index 656280f4a..7da5cb6fa 100644 --- a/docs/guide/http/middleware.md +++ b/docs/guide/http/middleware.md @@ -49,7 +49,7 @@ Which is registered like this (or as described in [`Registering Middleware by Me opts.AddMiddlewareByMessageType(typeof(FakeAuthenticationMiddleware)); opts.AddMiddlewareByMessageType(typeof(CanShipOrderMiddleWare)); ``` -snippet source | anchor +snippet source | anchor The key point to notice there is that `IResult` is a "return value" of the middleware. In the case of an HTTP endpoint, diff --git a/docs/guide/http/multi-tenancy.md b/docs/guide/http/multi-tenancy.md index 7cae520ed..02e1eba27 100644 --- a/docs/guide/http/multi-tenancy.md +++ b/docs/guide/http/multi-tenancy.md @@ -269,7 +269,7 @@ public static string NoTenantNoProblem() return "hey"; } ``` -snippet source | anchor +snippet source | anchor If the above usage completely disabled all tenant id detection or validation, in the case of an endpoint that *might* be @@ -287,7 +287,7 @@ public static string MaybeTenanted(IMessageBus bus) return bus.TenantId ?? "none"; } ``` -snippet source | anchor +snippet source | anchor diff --git a/docs/guide/http/policies.md b/docs/guide/http/policies.md index 3a3d5dc26..55bcd8e3d 100644 --- a/docs/guide/http/policies.md +++ b/docs/guide/http/policies.md @@ -65,7 +65,7 @@ app.MapWolverineEndpoints(opts => // Wolverine.Http.FluentValidation opts.UseFluentValidationProblemDetailMiddleware(); ``` -snippet source | anchor +snippet source | anchor The `HttpChain` model is a configuration time structure that Wolverine.Http will use at runtime to create the full @@ -97,7 +97,7 @@ app.MapWolverineEndpoints(opts => // Wolverine.Http.FluentValidation opts.UseFluentValidationProblemDetailMiddleware(); ``` -snippet source | anchor +snippet source | anchor ## Resource Writer Policies @@ -132,7 +132,7 @@ If you need special handling of a primary return type you can implement `IResour ```cs opts.AddResourceWriterPolicy(); ``` -snippet source | anchor +snippet source | anchor Resource writer policies registered this way will be applied in order before all built in policies. diff --git a/docs/guide/http/problemdetails.md b/docs/guide/http/problemdetails.md index 0869049c2..45b9ee9ae 100644 --- a/docs/guide/http/problemdetails.md +++ b/docs/guide/http/problemdetails.md @@ -137,7 +137,7 @@ public static ProblemDetails Before(IShipOrder command, Order order) return WolverineContinue.NoProblems; } ``` -snippet source | anchor +snippet source | anchor ## Within Message Handlers diff --git a/docs/guide/http/querystring.md b/docs/guide/http/querystring.md index 0cb4ef3c9..b8508da5b 100644 --- a/docs/guide/http/querystring.md +++ b/docs/guide/http/querystring.md @@ -120,7 +120,7 @@ public static class QueryOrdersEndpoint } } ``` -snippet source | anchor +snippet source | anchor Because we've used the `[FromQuery]` attribute on a parameter argument that's not a simple type, Wolverine is trying to bind diff --git a/docs/guide/http/security.md b/docs/guide/http/security.md index be0fd01dd..6e3de66bc 100644 --- a/docs/guide/http/security.md +++ b/docs/guide/http/security.md @@ -26,5 +26,5 @@ public void RequireAuthorizeOnAll() ConfigureEndpoints(e => e.RequireAuthorization()); } ``` -snippet source | anchor +snippet source | anchor diff --git a/docs/guide/messages.md b/docs/guide/messages.md index c9f72ad29..bb434cc80 100644 --- a/docs/guide/messages.md +++ b/docs/guide/messages.md @@ -100,9 +100,9 @@ types that will be published by the application by either implementing one of th ```cs public record CreateIssue(string Name) : IMessage; -public record DeleteIssue(Guid Id) : ICommand; +public record DeleteIssue(Guid Id) : IMessage; -public record IssueCreated(Guid Id, string Name) : IEvent; +public record IssueCreated(Guid Id, string Name) : IMessage; ``` snippet source | anchor diff --git a/docs/guide/messaging/broadcast-to-topic.md b/docs/guide/messaging/broadcast-to-topic.md index b4b36b9ce..88da3c654 100644 --- a/docs/guide/messaging/broadcast-to-topic.md +++ b/docs/guide/messaging/broadcast-to-topic.md @@ -20,14 +20,15 @@ theSender = Host.CreateDefaultBuilder() exchange.BindTopic("special").ToQueue("green"); }); - opts.Discovery.DisableConventionalDiscovery(); + opts.Discovery.DisableConventionalDiscovery() + .IncludeType(); opts.ServiceName = "TheSender"; opts.PublishMessagesToRabbitMqExchange("wolverine.topics", m => m.TopicName); }).Start(); ``` -snippet source | anchor +snippet source | anchor You can explicitly publish a message to a topic through this syntax: @@ -40,7 +41,7 @@ var publisher = theSender.Services await publisher.BroadcastToTopicAsync("color.purple", new Message1()); ``` -snippet source | anchor +snippet source | anchor ```cs var publisher = theSender.Services @@ -48,7 +49,7 @@ var publisher = theSender.Services await publisher.BroadcastToTopicAsync("color.purple", new Message1()); ``` -snippet source | anchor +snippet source | anchor diff --git a/docs/guide/messaging/expiration.md b/docs/guide/messaging/expiration.md index ca232362b..8c7a28197 100644 --- a/docs/guide/messaging/expiration.md +++ b/docs/guide/messaging/expiration.md @@ -20,7 +20,7 @@ public DateTimeOffset? DeliverBy set => _deliverBy = value?.ToUniversalTime(); } ``` -snippet source | anchor +snippet source | anchor At runtime, Wolverine will: diff --git a/docs/guide/messaging/subscriptions.md b/docs/guide/messaging/subscriptions.md index 732dc35c9..dbfb1e5f8 100644 --- a/docs/guide/messaging/subscriptions.md +++ b/docs/guide/messaging/subscriptions.md @@ -225,9 +225,11 @@ public interface IMessageRoute { Envelope CreateForSending(object message, DeliveryOptions? options, ISendingAgent localDurableQueue, WolverineRuntime runtime, string? topicName); + + MessageSubscriptionDescriptor Describe(); } ``` -snippet source | anchor +snippet source | anchor This type "knows" about any endpoint or model sending customizations like delivery expiration diff --git a/docs/guide/messaging/transports/azureservicebus/conventional-routing.md b/docs/guide/messaging/transports/azureservicebus/conventional-routing.md index 7355f608c..628cb1df2 100644 --- a/docs/guide/messaging/transports/azureservicebus/conventional-routing.md +++ b/docs/guide/messaging/transports/azureservicebus/conventional-routing.md @@ -76,12 +76,17 @@ opts.UseAzureServiceBusTesting() // its applicability to types // as well as overriding any listener, sender, topic, or subscription // options + + // Can't use the full name because of limitations on name length + convention.SubscriptionNameForListener(t => t.Name.ToLowerInvariant()); + convention.TopicNameForListener(t => t.Name.ToLowerInvariant()); + convention.TopicNameForSender(t => t.Name.ToLowerInvariant()); }) .AutoProvision() .AutoPurgeOnStartup(); ``` -snippet source | anchor +snippet source | anchor diff --git a/docs/guide/messaging/transports/external-tables.md b/docs/guide/messaging/transports/external-tables.md index e0a3ac390..221a821df 100644 --- a/docs/guide/messaging/transports/external-tables.md +++ b/docs/guide/messaging/transports/external-tables.md @@ -74,7 +74,7 @@ builder.UseWolverine(opts => .Sequential(); }); ``` -snippet source | anchor +snippet source | anchor So a couple things to know: diff --git a/docs/guide/messaging/transports/kafka.md b/docs/guide/messaging/transports/kafka.md index 31a04b516..dc7911356 100644 --- a/docs/guide/messaging/transports/kafka.md +++ b/docs/guide/messaging/transports/kafka.md @@ -98,7 +98,7 @@ using var host = await Host.CreateDefaultBuilder() opts.Services.AddResourceSetupOnStartup(); }).StartAsync(); ``` -snippet source | anchor +snippet source | anchor The various `Configure*****()` methods provide quick access to the full API of the Confluent Kafka library for security @@ -187,5 +187,5 @@ public static class KafkaInstrumentation } } ``` -snippet source | anchor +snippet source | anchor diff --git a/docs/guide/messaging/transports/mqtt.md b/docs/guide/messaging/transports/mqtt.md index 337ac44e9..22b5d3f64 100644 --- a/docs/guide/messaging/transports/mqtt.md +++ b/docs/guide/messaging/transports/mqtt.md @@ -141,7 +141,7 @@ public class FirstMessage public Guid Id { get; set; } = Guid.NewGuid(); } ``` -snippet source | anchor +snippet source | anchor ## Publishing by Topic Rules diff --git a/docs/guide/messaging/transports/rabbitmq/index.md b/docs/guide/messaging/transports/rabbitmq/index.md index 5b0556528..6b225361b 100644 --- a/docs/guide/messaging/transports/rabbitmq/index.md +++ b/docs/guide/messaging/transports/rabbitmq/index.md @@ -16,9 +16,10 @@ To use [RabbitMQ](http://www.rabbitmq.com/) as a transport with Wolverine, first return await Host.CreateDefaultBuilder(args) .UseWolverine(opts => { + opts.ApplicationAssembly = typeof(Program).Assembly; + // Listen for messages coming into the pongs queue - opts - .ListenToRabbitQueue("pongs"); + opts.ListenToRabbitQueue("pongs"); // Publish messages to the pings queue opts.PublishMessage().ToRabbitExchange("pings"); @@ -40,7 +41,7 @@ return await Host.CreateDefaultBuilder(args) opts.Services.AddHostedService(); }).RunJasperFxCommands(args); ``` -snippet source | anchor +snippet source | anchor See the [Rabbit MQ .NET Client documentation](https://www.com/dotnet-api-guide.html#connecting) for more information about configuring the `ConnectionFactory` to connect to Rabbit MQ. diff --git a/docs/guide/messaging/transports/rabbitmq/topics.md b/docs/guide/messaging/transports/rabbitmq/topics.md index 73b33cfbc..a87bdc093 100644 --- a/docs/guide/messaging/transports/rabbitmq/topics.md +++ b/docs/guide/messaging/transports/rabbitmq/topics.md @@ -60,7 +60,7 @@ public class FirstMessage public Guid Id { get; set; } = Guid.NewGuid(); } ``` -snippet source | anchor +snippet source | anchor Of course, you can always explicitly send a message to a specific topic with this syntax: @@ -100,14 +100,15 @@ theSender = Host.CreateDefaultBuilder() exchange.BindTopic("special").ToQueue("green"); }); - opts.Discovery.DisableConventionalDiscovery(); + opts.Discovery.DisableConventionalDiscovery() + .IncludeType(); opts.ServiceName = "TheSender"; opts.PublishMessagesToRabbitMqExchange("wolverine.topics", m => m.TopicName); }).Start(); ``` -snippet source | anchor +snippet source | anchor ## Publishing by Topic Rule diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 6373e5484..42c544325 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -114,7 +114,7 @@ var builder = WebApplication.CreateBuilder(args); // will assert this is missing on startup:( builder.Services.AddWolverineHttp(); ``` -snippet source | anchor +snippet source | anchor Also for Wolverine.Http users, the `[Document]` attribute behavior in the Marten integration is now "required by default." diff --git a/docs/guide/runtime.md b/docs/guide/runtime.md index 73e3b6f83..d6374380a 100644 --- a/docs/guide/runtime.md +++ b/docs/guide/runtime.md @@ -176,7 +176,7 @@ take advantage of the persistent outbox mechanism in Wolverine. To opt into maki // I overrode the buffering limits just to show // that they exist for "back pressure" opts.ListenToAzureServiceBusQueue("incoming") - .UseDurableInbox(new BufferingLimits(1000, 200)); +.UseDurableInbox(new BufferingLimits(1000, 200)); opts.PublishAllMessages().ToAzureServiceBusQueue("outgoing") .UseDurableOutbox(); diff --git a/docs/tutorials/cqrs-with-marten.md b/docs/tutorials/cqrs-with-marten.md index 527aa5926..140b04757 100644 --- a/docs/tutorials/cqrs-with-marten.md +++ b/docs/tutorials/cqrs-with-marten.md @@ -129,7 +129,7 @@ public class Incident public bool ShouldDelete(Archived @event) => true; } ``` -snippet source | anchor +snippet source | anchor ::: info @@ -168,7 +168,7 @@ public record IncidentClosed( Guid ClosedBy ); ``` -snippet source | anchor +snippet source | anchor Many people -- myself included -- prefer to use `record` types for the event types. I would deviate from that though diff --git a/docs/tutorials/modular-monolith.md b/docs/tutorials/modular-monolith.md index fd944ea4f..43dc73a64 100644 --- a/docs/tutorials/modular-monolith.md +++ b/docs/tutorials/modular-monolith.md @@ -83,6 +83,11 @@ builder.Services.AddMarten(opts => builder.UseWolverine(opts => { + // This helps Wolverine to use a unified envelope storage across all + // modules, which in turn should help Wolverine be more efficient with + // your database + opts.Durability.MessageStorageSchemaName = "wolverine"; + // Tell Wolverine that when you have more than one handler for the same // message type, they should be executed separately and automatically // "stuck" to separate local queues @@ -100,7 +105,7 @@ builder.UseWolverine(opts => opts.Policies.AutoApplyTransactions(); }); ``` -snippet source | anchor +snippet source | anchor See [Message Identity](/guide/durability/#message-identity) and [Multiple Handlers for the Same Message Type](/guide/handlers/#multiple-handlers-for-the-same-message-type) @@ -439,9 +444,11 @@ by using this setting: ```cs // THIS IS IMPORTANT FOR MODULAR MONOLITH USAGE! +// This helps Wolverine out to always utilize the same envelope storage +// for all modules for more efficient usage of resources opts.Durability.MessageStorageSchemaName = "wolverine"; ``` -snippet source | anchor +snippet source | anchor By setting any value for `WolverineOptions.Durability.MessageStorageSchemaName`, Wolverine will use that value for the database schema diff --git a/docs/tutorials/ping-pong.md b/docs/tutorials/ping-pong.md index e657c8965..302ce3435 100644 --- a/docs/tutorials/ping-pong.md +++ b/docs/tutorials/ping-pong.md @@ -66,29 +66,31 @@ namespace Pinger; public class Worker : BackgroundService { private readonly ILogger _logger; - private readonly IMessageBus _bus; + private readonly IServiceProvider _serviceProvider; - public Worker(ILogger logger, IMessageBus bus) + public Worker(ILogger logger, IServiceProvider serviceProvider) { _logger = logger; - _bus = bus; + _serviceProvider = serviceProvider; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var pingNumber = 1; + await using var scope= _serviceProvider.CreateAsyncScope(); + var bus = scope.ServiceProvider.GetRequiredService(); while (!stoppingToken.IsCancellationRequested) { await Task.Delay(1000, stoppingToken); _logger.LogInformation("Sending Ping #{Number}", pingNumber); - await _bus.PublishAsync(new Ping { Number = pingNumber }); + await bus.PublishAsync(new Ping { Number = pingNumber }); pingNumber++; } } } ``` -snippet source | anchor +snippet source | anchor and lastly a message handler for any `Pong` messages coming back from the `Ponger` we'll build next: @@ -125,12 +127,14 @@ using Wolverine.Transports.Tcp; return await Host.CreateDefaultBuilder(args) .UseWolverine(opts => { + opts.ApplicationAssembly = typeof(Program).Assembly; + // Using Wolverine's built in TCP transport opts.ListenAtPort(5581); }) .RunJasperFxCommands(args); ``` -snippet source | anchor +snippet source | anchor And a message handler for the `Ping` messages that will turn right around and shoot a `Pong` response right back diff --git a/src/Persistence/EfCoreTests/MultiTenancy/MultiTenancyDocumentationSamples.cs b/src/Persistence/EfCoreTests/MultiTenancy/MultiTenancyDocumentationSamples.cs index cd5937259..11d201d01 100644 --- a/src/Persistence/EfCoreTests/MultiTenancy/MultiTenancyDocumentationSamples.cs +++ b/src/Persistence/EfCoreTests/MultiTenancy/MultiTenancyDocumentationSamples.cs @@ -1,9 +1,14 @@ +using JasperFx; using JasperFx.MultiTenancy; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Npgsql; +using SharedPersistenceModels.Items; +using SharedPersistenceModels.Orders; using Wolverine; +using Wolverine.EntityFrameworkCore; using Wolverine.Postgresql; using Wolverine.SqlServer; @@ -33,6 +38,11 @@ public async Task static_postgresql() tenants.Register("tenant2", configuration.GetConnectionString("tenant2")); tenants.Register("tenant3", configuration.GetConnectionString("tenant3")); }); + + opts.Services.AddDbContextWithWolverineManagedMultiTenancy((builder, connectionString, _) => + { + builder.UseNpgsql(connectionString.Value, b => b.MigrationsAssembly("MultiTenantedEfCoreWithPostgreSQL")); + }, AutoCreate.CreateOrUpdate); }); #endregion @@ -60,6 +70,18 @@ public async Task static_sqlserver() tenants.Register("tenant2", configuration.GetConnectionString("tenant2")); tenants.Register("tenant3", configuration.GetConnectionString("tenant3")); }); + + // Just to show that you *can* use more than one DbContext + opts.Services.AddDbContextWithWolverineManagedMultiTenancy((builder, connectionString, _) => + { + // You might have to set the migration assembly + builder.UseSqlServer(connectionString.Value, b => b.MigrationsAssembly("MultiTenantedEfCoreWithSqlServer")); + }, AutoCreate.CreateOrUpdate); + + opts.Services.AddDbContextWithWolverineManagedMultiTenancy((builder, connectionString, _) => + { + builder.UseSqlServer(connectionString.Value, b => b.MigrationsAssembly("MultiTenantedEfCoreWithSqlServer")); + }, AutoCreate.CreateOrUpdate); }); #endregion @@ -88,6 +110,7 @@ public void dynamic_multi_tenancy_with_postgresql() seed.Register("tenant2", configuration.GetConnectionString("tenant2")); seed.Register("tenant3", configuration.GetConnectionString("tenant3")); }); + }); #endregion diff --git a/src/Persistence/EfCoreTests/MultiTenancy/multi_tenancy_with_marten_managed_multi_tenancy.cs b/src/Persistence/EfCoreTests/MultiTenancy/multi_tenancy_with_marten_managed_multi_tenancy.cs index 81a14bcf8..7c94ddf6b 100644 --- a/src/Persistence/EfCoreTests/MultiTenancy/multi_tenancy_with_marten_managed_multi_tenancy.cs +++ b/src/Persistence/EfCoreTests/MultiTenancy/multi_tenancy_with_marten_managed_multi_tenancy.cs @@ -21,6 +21,8 @@ public multi_tenancy_with_marten_managed_multi_tenancy() : base(DatabaseEngine.P public override void Configure(WolverineOptions opts) { + #region sample_use_multi_tenancy_with_both_marten_and_ef_core + opts.Services.AddMarten(m => { m.MultiTenantedDatabases(x => @@ -38,6 +40,8 @@ public override void Configure(WolverineOptions opts) { builder.UseNpgsql(dataSource, b => b.MigrationsAssembly("MultiTenantedEfCoreWithPostgreSQL")); }, AutoCreate.CreateOrUpdate); + + #endregion // Little weird, but we have to remove this DbContext to use // the lightweight saga persistence diff --git a/src/Persistence/EfCoreTests/SampleUsageWithAutoApplyTransactions.cs b/src/Persistence/EfCoreTests/SampleUsageWithAutoApplyTransactions.cs index 5a22fd6d3..9eb62a175 100644 --- a/src/Persistence/EfCoreTests/SampleUsageWithAutoApplyTransactions.cs +++ b/src/Persistence/EfCoreTests/SampleUsageWithAutoApplyTransactions.cs @@ -1,9 +1,11 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using SharedPersistenceModels.Items; using Wolverine; using Wolverine.EntityFrameworkCore; +using Wolverine.SqlServer; namespace EfCoreTests; @@ -23,8 +25,6 @@ public static async Task bootstrap() x.UseSqlServer(connectionString); }); - opts.UseEntityFrameworkCoreTransactions(); - // Add the auto transaction middleware attachment policy opts.Policies.AutoApplyTransactions(); }); @@ -34,4 +34,63 @@ public static async Task bootstrap() #endregion } + + public static async Task quickstart() + { + #region sample_getting_started_with_efcore + + var builder = Host.CreateApplicationBuilder(); + + var connectionString = builder.Configuration.GetConnectionString("sqlserver"); + + // Register a DbContext or multiple DbContext types as normal + builder.Services.AddDbContext( + x => x.UseSqlServer(connectionString), + + // This is actually a significant performance gain + // for Wolverine's sake + optionsLifetime:ServiceLifetime.Singleton); + + // Register Wolverine + builder.UseWolverine(opts => + { + // You'll need to independently tell Wolverine where and how to + // store messages as part of the transactional inbox/outbox + opts.PersistMessagesWithSqlServer(connectionString); + + // Adding EF Core transactional middleware, saga support, + // and EF Core support for Wolverine storage operations + opts.UseEntityFrameworkCoreTransactions(); + }); + + // Rest of your bootstrapping... + + #endregion + } + + public static async Task quickstart2() + { + + #region sample_idiomatic_wolverine_registration_of_ef_core + + var builder = Host.CreateApplicationBuilder(); + + var connectionString = builder.Configuration.GetConnectionString("sqlserver"); + + builder.UseWolverine(opts => + { + // You'll need to independently tell Wolverine where and how to + // store messages as part of the transactional inbox/outbox + opts.PersistMessagesWithSqlServer(connectionString); + + // Registers the DbContext type in your IoC container, sets the DbContextOptions + // lifetime to "Singleton" to optimize Wolverine usage, and also makes sure that + // your Wolverine service has all the EF Core transactional middleware, saga support, + // and storage operation helpers activated for this application + opts.Services.AddDbContextWithWolverineIntegration( + x => x.UseSqlServer(connectionString)); + }); + + #endregion + } } \ No newline at end of file diff --git a/src/Persistence/EfCoreTests/using_storage_return_types_and_entity_attributes.cs b/src/Persistence/EfCoreTests/using_storage_return_types_and_entity_attributes.cs index 1477cfb44..65b73a1db 100644 --- a/src/Persistence/EfCoreTests/using_storage_return_types_and_entity_attributes.cs +++ b/src/Persistence/EfCoreTests/using_storage_return_types_and_entity_attributes.cs @@ -52,6 +52,8 @@ public override async Task Persist(Todo todo) } } +#region sample_TodoDbContext + public class TodoDbContext : DbContext { public TodoDbContext(DbContextOptions options) : base(options) @@ -70,4 +72,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) map.Property(x => x.IsComplete).HasColumnName("is_complete"); }); } -} \ No newline at end of file +} + +#endregion \ No newline at end of file diff --git a/src/Samples/EFCoreSample/ItemService/Orders/ApproveOrder.cs b/src/Samples/EFCoreSample/ItemService/Orders/ApproveOrder.cs new file mode 100644 index 000000000..2848420aa --- /dev/null +++ b/src/Samples/EFCoreSample/ItemService/Orders/ApproveOrder.cs @@ -0,0 +1,3 @@ +namespace ItemService.Orders; + +public record ApproveOrder(string OrderId, string CustomerId); \ No newline at end of file diff --git a/src/Samples/EFCoreSample/ItemService/Orders/CreditLimitExceeded.cs b/src/Samples/EFCoreSample/ItemService/Orders/CreditLimitExceeded.cs new file mode 100644 index 000000000..7ce7e8528 --- /dev/null +++ b/src/Samples/EFCoreSample/ItemService/Orders/CreditLimitExceeded.cs @@ -0,0 +1,3 @@ +namespace ItemService.Orders; + +public record CreditLimitExceeded(string OrderId, string CustomerId); \ No newline at end of file diff --git a/src/Samples/EFCoreSample/ItemService/Orders/CreditReserved.cs b/src/Samples/EFCoreSample/ItemService/Orders/CreditReserved.cs new file mode 100644 index 000000000..0da735823 --- /dev/null +++ b/src/Samples/EFCoreSample/ItemService/Orders/CreditReserved.cs @@ -0,0 +1,3 @@ +namespace ItemService.Orders; + +public record CreditReserved(string OrderId, string CustomerId); \ No newline at end of file diff --git a/src/Samples/EFCoreSample/ItemService/Orders/Order.cs b/src/Samples/EFCoreSample/ItemService/Orders/Order.cs new file mode 100644 index 000000000..e4d42e2af --- /dev/null +++ b/src/Samples/EFCoreSample/ItemService/Orders/Order.cs @@ -0,0 +1,108 @@ +using Microsoft.EntityFrameworkCore; +using Wolverine; + +namespace ItemService.Orders; + +#region sample_order_saga_for_efcore + +public enum OrderStatus +{ + Pending = 0, + CreditReserved = 1, + CreditLimitExceeded = 2, + Approved = 3, + Rejected = 4 +} + +public class Order : Saga +{ + public string? Id { get; set; } + public OrderStatus OrderStatus { get; set; } = OrderStatus.Pending; + + public object[] Start( + OrderPlaced orderPlaced, + ILogger logger + ) + { + Id = orderPlaced.OrderId; + logger.LogInformation("Order {OrderId} placed", Id); + OrderStatus = OrderStatus.Pending; + return + [ + new ReserveCredit( + orderPlaced.OrderId, + orderPlaced.CustomerId, + orderPlaced.Amount + ) + ]; + } + + public object[] Handle( + CreditReserved creditReserved, + ILogger logger + ) + { + OrderStatus = OrderStatus.CreditReserved; + logger.LogInformation("Credit reserver for Order {OrderId}", Id); + return [new ApproveOrder(creditReserved.OrderId, creditReserved.CustomerId)]; + } + + public void Handle( + OrderApproved orderApproved, + ILogger logger + ) + { + OrderStatus = OrderStatus.Approved; + logger.LogInformation("Order {OrderId} approved", Id); + } + + public object[] Handle( + CreditLimitExceeded creditLimitExceeded, + ILogger logger + ) + { + OrderStatus = OrderStatus.CreditLimitExceeded; + return [new RejectOrder(creditLimitExceeded.OrderId)]; + } + + public void Handle( + OrderRejected orderRejected, + ILogger logger + ) + { + OrderStatus = OrderStatus.Rejected; + logger.LogInformation("Order {OrderId} rejected", Id); + MarkCompleted(); + } +} + +#endregion + +#region sample_OrdersDbContext + +public class OrdersDbContext : DbContext +{ + protected OrdersDbContext() + { + } + + public OrdersDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Orders { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Your normal EF Core mapping + modelBuilder.Entity(map => + { + map.ToTable("orders", "sample"); + map.HasKey(x => x.Id); + map.Property(x => x.OrderStatus) + .HasConversion(v => v.ToString(), v => Enum.Parse(v)); + }); + } +} + +#endregion \ No newline at end of file diff --git a/src/Samples/EFCoreSample/ItemService/Orders/OrderApproved.cs b/src/Samples/EFCoreSample/ItemService/Orders/OrderApproved.cs new file mode 100644 index 000000000..a74e487b4 --- /dev/null +++ b/src/Samples/EFCoreSample/ItemService/Orders/OrderApproved.cs @@ -0,0 +1,3 @@ +namespace ItemService.Orders; + +public record OrderApproved(string OrderId); \ No newline at end of file diff --git a/src/Samples/EFCoreSample/ItemService/Orders/OrderCompleted.cs b/src/Samples/EFCoreSample/ItemService/Orders/OrderCompleted.cs new file mode 100644 index 000000000..713965a3e --- /dev/null +++ b/src/Samples/EFCoreSample/ItemService/Orders/OrderCompleted.cs @@ -0,0 +1,3 @@ +namespace ItemService.Orders; + +public record OrderCompleted(string OrderId); \ No newline at end of file diff --git a/src/Samples/EFCoreSample/ItemService/Orders/OrderCreated.cs b/src/Samples/EFCoreSample/ItemService/Orders/OrderCreated.cs new file mode 100644 index 000000000..ddc3c70d2 --- /dev/null +++ b/src/Samples/EFCoreSample/ItemService/Orders/OrderCreated.cs @@ -0,0 +1,3 @@ +namespace ItemService.Orders; + +public record OrderCreated(string OrderId, string CustomerId, string CustomerName); \ No newline at end of file diff --git a/src/Samples/EFCoreSample/ItemService/Orders/OrderPlaced.cs b/src/Samples/EFCoreSample/ItemService/Orders/OrderPlaced.cs new file mode 100644 index 000000000..b5b56fcd5 --- /dev/null +++ b/src/Samples/EFCoreSample/ItemService/Orders/OrderPlaced.cs @@ -0,0 +1,3 @@ +namespace ItemService.Orders; + +public record OrderPlaced(string OrderId, string CustomerId, decimal Amount); \ No newline at end of file diff --git a/src/Samples/EFCoreSample/ItemService/Orders/OrderRejected.cs b/src/Samples/EFCoreSample/ItemService/Orders/OrderRejected.cs new file mode 100644 index 000000000..03a20309a --- /dev/null +++ b/src/Samples/EFCoreSample/ItemService/Orders/OrderRejected.cs @@ -0,0 +1,3 @@ +namespace ItemService.Orders; + +public record OrderRejected(string OrderId); \ No newline at end of file diff --git a/src/Samples/EFCoreSample/ItemService/Orders/PlaceOrder.cs b/src/Samples/EFCoreSample/ItemService/Orders/PlaceOrder.cs new file mode 100644 index 000000000..7620f42a9 --- /dev/null +++ b/src/Samples/EFCoreSample/ItemService/Orders/PlaceOrder.cs @@ -0,0 +1,7 @@ +namespace ItemService.Orders; + +public record PlaceOrder( + string OrderId, + string CustomerId, + decimal Amount +); \ No newline at end of file diff --git a/src/Samples/EFCoreSample/ItemService/Orders/RejectOrder.cs b/src/Samples/EFCoreSample/ItemService/Orders/RejectOrder.cs new file mode 100644 index 000000000..a8a0617e7 --- /dev/null +++ b/src/Samples/EFCoreSample/ItemService/Orders/RejectOrder.cs @@ -0,0 +1,3 @@ +namespace ItemService.Orders; + +public record RejectOrder(string OrderId); \ No newline at end of file diff --git a/src/Samples/EFCoreSample/ItemService/Orders/ReservceCredit.cs b/src/Samples/EFCoreSample/ItemService/Orders/ReservceCredit.cs new file mode 100644 index 000000000..824404b13 --- /dev/null +++ b/src/Samples/EFCoreSample/ItemService/Orders/ReservceCredit.cs @@ -0,0 +1,3 @@ +namespace ItemService.Orders; + +public record ReserveCredit(string OrderId, string CustomerId, decimal Amount); \ No newline at end of file diff --git a/src/Testing/Wolverine.ComplianceTests/StorageActionCompliance.cs b/src/Testing/Wolverine.ComplianceTests/StorageActionCompliance.cs index 4575430d1..a905e5988 100644 --- a/src/Testing/Wolverine.ComplianceTests/StorageActionCompliance.cs +++ b/src/Testing/Wolverine.ComplianceTests/StorageActionCompliance.cs @@ -291,6 +291,8 @@ public record MaybeInsertTodo(string Id, string Name, bool ShouldInsert); public record ReturnNullInsert; public record ReturnNullStorageAction; +#region sample_TodoHandler_to_demonstrate_storage_operations + public static class TodoHandler { public static Insert Handle(CreateTodo command) => Storage.Insert(new Todo @@ -389,6 +391,8 @@ public static IStorageAction Handle(MaybeCompleteTodo command, [Entity(Req } } +#endregion + public record CompleteTodo(string Id); public record MaybeCompleteTodo(string Id);