diff --git a/docs/guide/codegen.md b/docs/guide/codegen.md index 821641b0d..608fc1cb8 100644 --- a/docs/guide/codegen.md +++ b/docs/guide/codegen.md @@ -5,10 +5,6 @@ If you are experiencing noticeable startup lags or seeing spikes in memory utili Wolverine, you will want to pursue using either the `Auto` or `Static` modes for code generation as explained in this guide. ::: -::: tip -This blog post from Oskar Dudycz will apply to Wolverine as well: [How to create a Docker image for the Marten application](https://event-driven.io/en/marten_and_docker/) -::: - Wolverine uses runtime code generation to create the "adaptor" code that Wolverine uses to call into your message handlers. Wolverine's [middleware strategy](/guide/handlers/middleware) also uses this strategy to "weave" calls to middleware directly into the runtime pipeline without requiring the copious usage of adapter interfaces @@ -25,7 +21,7 @@ Not to worry though, Wolverine has several facilities to either preview the gene really understand how Wolverine is interacting with your code and to optimize the "cold start" by generating the dynamic code ahead of time so that it can be embedded directly into your application's main assembly and discovered from there. -By default, Wolverine runs with "dynamic" code generation where all the necessary generated types are built on demand +By default, Wolverine runs with "dynamic" code generation where a l the necessary generated types are built on demand the first time they are needed. This is perfect for a quick start to Wolverine, and might be fine in smaller projects even at production time. @@ -81,6 +77,8 @@ Most of the facilities shown here will require the [Oakton command line integrat ## Embedding Codegen in Docker +This blog post from Oskar Dudycz will apply to Wolverine as well: [How to create a Docker image for the Marten application](https://event-driven.io/en/marten_and_docker/) + At this point, the most successful mechanism and sweet spot is to run the codegen as `Dynamic` at development time, but generating the code artifacts just in time for production deployments. From Wolverine's sibling project Marten, see this section on [Application project setup](https://martendb.io/devops/devops.html#application-project-set-up) for embedding the code generation directly into your Docker images for deployment. @@ -124,6 +122,122 @@ using var host = Host.CreateDefaultBuilder() If the assembly choice is correct, and the expected code files are really in `Internal/Generated` exactly as you'd expect, make sure there's no accidental `` nodes in your project file. *Don't laugh, that's actually happened to Wolverine users* +## Wolverine Code Generation and IoC + +::: info +Why, you ask, does Wolverine do any of this? Wolverine was originally conceived of as the successor to the +[FubuMVC & FubuTransportation](https://fubumvc.github.io) projects from the early 2010's. A major lesson learned +from FubuMVC was that we needed to reduce object allocations, layering, runaway `Exception` stack traces, and allow +for more flexible and streamlined handler or endpoint method signatures. To that end we fully embraced using runtime code +generation -- and this was built well before source generators were available. + +As for the IoC part of this strategy, we ask you, what's the very fastest IoC tool in .NET? The answer of course, is +"no IoC container." +::: + +Wolverine's code generation uses the configuration of your IoC tool to create the generated code wrappers +around your raw message handlers, HTTP endpoints, and middleware methods. Whenever possible, Wolverine is trying to +completely eliminate your application's IoC tool from the runtime code by generating the necessary constructor function +invocations to exactly mimic your application's IoC configuration. + +::: info +Because you should care about this, Wolverine is absolutely generating `using` or `await using` for any objects it +creates through constructor calls that implements `IDisposable` or `IAsyncDisposable`. +::: + +When generating the adapter classes, Wolverine can infer which method arguments or type dependencies can be sourced +from your application's IoC container configuration. If Wolverine can determine a way to generate all the necessary +constructor calls to create any necessary services registered with a `Scoped` or `Transient` lifetime, Wolverine will generate +code with the constructors. In this case, any IoC services that are registered with a `Singleton` lifetime +will be "inlined" as constructor arguments into the generated adapter class itself for a little better efficiency. + +::: warning +The usage of a service locator within the generated code will naturally be a little less efficient just because there +is more runtime overhead. More dangerously, the service locator usage can sometimes foul up the scoping of services +like Wolverine's `IMessageBus` or Marten's `IDocumentSession` that are normally built outside of the IoC container +::: + +If Wolverine cannot determine a path to generate +code for raw constructor construction of any registered services for a message handler or HTTP endpoint, Wolverine +will fall back to generating code with the [service locator pattern](https://en.wikipedia.org/wiki/Service_locator_pattern) +using a scoped container (think [IServiceScopeFactory](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicescopefactory?view=net-9.0-pp)). + +Here's some facts you do need to know about this whole process: + +* The adapter classes generated by Wolverine for both message handlers and HTTP endpoints are effectively singleton + scoped and only ever built once +* Wolverine will try to bring `Singleton` scoped services through the generated adapter type's constructor function *one time* +* Wolverine will have to fall back to the service locator usage if any service dependency that has a `Scoped` or `Transient` + lifetime is either an `internal` type or uses an "opaque" Lambda registration (think `IServiceCollection.AddScoped(s => {})`) + +::: tip +The code generation using IoC configuration is tested with both the built in .NET `ServiceProvider` and [Lamar](https://jasperfx.github.io/lamar). It +is theoretically possible to use other IoC tools with Wolverine, but only if you are *only* using `IServiceCollection` +for your IoC configuration. +::: + +As of Wolverine 5.0, you now have the ability to better control the usage of the service locator in Wolverine's +code generation to potentially avoid unwanted usage: + + + +```cs +var builder = Host.CreateApplicationBuilder(); +builder.UseWolverine(opts => +{ + // This is the default behavior. Wolverine will allow you to utilize + // service location in the codegen, but will warn you through log messages + // when this happens + opts.ServiceLocationPolicy = ServiceLocationPolicy.AllowedButWarn; + + // Tell Wolverine to just be quiet about service location and let it + // all go. For any of you with small children, I defy you to get the + // Frozen song out of your head now... + opts.ServiceLocationPolicy = ServiceLocationPolicy.AlwaysAllowed; + + // Wolverine will throw exceptions at runtime if it encounters + // a message handler or HTTP endpoint that would require service + // location in the code generation + // Use this option to disallow any undesirably service location + opts.ServiceLocationPolicy = ServiceLocationPolicy.NotAllowed; +}); +``` +snippet source | anchor + + +::: note +[Wolverine.HTTP has some additional control over the service locator](/guide/http/#using-the-httpcontext-requestservices) to utilize the shared scoped container +with the rest of the AspNetCore pipeline. +::: + +## Allow List for Service Location + +Wolverine always reverts to using a service locator when it encounters an "opaque" Lambda registration that has either +a `Scoped` or `Transient` service lifetime. You can explicitly create an "allow" list of service types that can use +a service locator pattern while allowing the rest of the code generation for the message handler or HTTP endpoint to use +the more predictable and efficient generated constructor functions with this syntax: + + + +```cs +var builder = Host.CreateApplicationBuilder(); +builder.UseWolverine(opts => +{ + // other configuration + + // Use a service locator for this service w/o forcing the entire + // message handler adapter to use a service locator for everything + opts.CodeGeneration.AlwaysUseServiceLocationFor(); +}); +``` +snippet source | anchor + + +For example, this functionality might be helpful for: + +* [Refit proxies](https://github.com/reactiveui/refit) that are registered in IoC with a Lambda registration, but might not use any other services +* EF Core `DbContext` types that might require some runtime configuration to construct themselves, but don't use other services (a [JasperFx Software](https://jasperfx.net) client + ran into this needing to conditionally opt into read replica usage, so hence, this feature made it into Wolverine 5.0) ## Environment Check for Expected Types @@ -157,7 +271,8 @@ await host.StartAsync(); Do note that you would have to opt into using the environment checks on application startup, and maybe even force .NET to make hosted service failures stop the application. -See [Oakton's Environment Check functionality](https://jasperfx.github.io/oakton/guide/host/environment.html) for more information. +See [Oakton's Environment Check functionality](https://jasperfx.github.io/oakton/guide/host/environment.html) for more information (the old Oakton documentation is still relevant for +JasperFx). ## Previewing the Generated Code @@ -185,11 +300,9 @@ directly under the root of your project folder. ## Optimized Workflow -::: info -Optimized Workflow overrides the storage migration [AutoBuildMessageStorageOnStartup](./durability/managing#disable-automatic-storage-migration) option, making it enabled for "Development" environment and disabled for other environments -::: - -Or as a short hand option, use this: +Wolverine and [Marten](https://martendb.io) both use the shared JasperFx library for their code generation, +and you can configure different behavior for production versus development time for both tools (and any future +"CritterStack" tools) with this usage: diff --git a/docs/guide/durability/efcore/sagas.md b/docs/guide/durability/efcore/sagas.md index 29b075ce8..e0e4af151 100644 --- a/docs/guide/durability/efcore/sagas.md +++ b/docs/guide/durability/efcore/sagas.md @@ -112,12 +112,9 @@ public class OrdersDbContext : DbContext 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)); - - // enable optimistic concurrency - map.Property(x => x.Version) - .IsConcurrencyToken(); }); } } diff --git a/docs/guide/durability/marten/event-sourcing.md b/docs/guide/durability/marten/event-sourcing.md index c408c8284..6eb00522c 100644 --- a/docs/guide/durability/marten/event-sourcing.md +++ b/docs/guide/durability/marten/event-sourcing.md @@ -6,6 +6,14 @@ need in a message handler or HTTP endpoint is a read-only copy of an event strea instead that has a little bit lighter weight runtime within Marten. ::: +::: info +If your message handler or HTTP endpoint uses more than one declarative attribute for retrieving Marten data, +Wolverine 5.0+ is able to utilize [Marten's Batch Querying capability](https://martendb.io/documents/querying/batched-queries.html#batched-queries) for more efficient interaction with the database. + +This batching behavior is also supported for all the declarative attributes and the "aggregate handler workflow" in general +described in this page. +::: + See the [OrderEventSourcingSample project on GitHub](https://github.com/JasperFx/wolverine/tree/main/src/Persistence/OrderEventSourcingSample) for more samples. That Wolverine + Marten combination is optimized for efficient and productive development using a [CQRS architecture style](https://martinfowler.com/bliki/CQRS.html) with [Marten's event sourcing](https://martendb.io/events/) support. diff --git a/docs/guide/durability/marten/index.md b/docs/guide/durability/marten/index.md index e7616efde..9461a0f6d 100644 --- a/docs/guide/durability/marten/index.md +++ b/docs/guide/durability/marten/index.md @@ -104,6 +104,11 @@ Using the `IntegrateWithWolverine()` extension method behind your call to `AddMa ## Entity Attribute Loading +::: info +If your message handler or HTTP endpoint uses more than one declarative attribute for retrieving Marten data, +Wolverine 5.0+ is able to utilize [Marten's Batch Querying capability](https://martendb.io/documents/querying/batched-queries.html#batched-queries) for more efficient interaction with the database. +::: + The Marten integration is able to completely support the [Entity attribute usage](/guide/handlers/persistence.html#automatically-loading-entities-to-method-parameters). ## Marten as Outbox diff --git a/docs/guide/http/index.md b/docs/guide/http/index.md index 29cf21b8c..3b9b42690 100644 --- a/docs/guide/http/index.md +++ b/docs/guide/http/index.md @@ -204,5 +204,73 @@ return await app.RunJasperFxCommands(args); snippet source | anchor +## Using the HttpContext.RequestServices + +::: tip +The opt in behavior to share the scoped services with the rest of the AspNetCore pipeline is useful +for using Wolverine endpoints underneath AspNetCore middleware that "smuggles" state through the IoC container. + +Custom multi-tenancy middleware or custom authorization or other security middleware frequently does this. We think +this will be helpful for mixed systems where Wolverine.HTTP is used for some routes while other routes are served +by MVC Core or Minimal API or even some other kind of AspNetCore `Endpoint`. +::: + +By default, any time [Wolverine has to revert to using a service locator](/guide/codegen.html#wolverine-code-generation-and-ioc) +to generate the adapter code for an HTTP endpoint, Wolverine is using an isolated `IServiceScope` (or Lamar `INestedContainer`) within the generated code. + +But, with Wolverine 5.0+ you can opt into Wolverine just using the `HttpContext.RequestServices` so that you +can share services with AspNetCore middleware. You can also configure *some* service types to be pulled from +the `HttpContext.RequestServices` even if Wolverine is otherwise generating more efficient constructor calls +for all other dependencies. Here's an example using both of these opt in behaviors: + + + +```cs +var builder = WebApplication.CreateBuilder(); + +builder.UseWolverine(opts => +{ + // more configuration +}); + +// Just pretend that this IUserContext is being +builder.Services.AddScoped(); +builder.Services.AddWolverineHttp(); + +var app = builder.Build(); + +// Custom middleware that is somehow configuring our IUserContext +// that might be getting used within +app.UseMiddleware(); + +app.MapWolverineEndpoints(opts => +{ + // Opt into using the shared HttpContext.RequestServices scoped + // container any time Wolverine has to use a service locator + opts.ServiceProviderSource = ServiceProviderSource.FromHttpContextRequestServices; + + // OR this is the default behavior to be backwards compatible: + opts.ServiceProviderSource = ServiceProviderSource.IsolatedAndScoped; + + // We're telling Wolverine that the IUserContext should always + // be pulled from HttpContext.RequestServices + // and this happens regardless of the ServerProviderSource! + opts.SourceServiceFromHttpContext(); +}); + +return await app.RunJasperFxCommands(args); +``` +snippet source | anchor + + +Notice the call to `SourceServiceFromHttpContext()`. That directs Wolverine.HTTP to always pull the service +`T` from the `HttpContext.RequestServices` scoped container so that Wolverine.HTTP can play nicely with custom AspNetCore +middleware or whatever else you have around your Wolverine.HTTP endpoints. + +::: warning +The Wolverine team believes that smuggling important state between upstream middleware and downstream handlers +leads to code that is hard to reason about and hence, potentially buggy in real life usage. Alas, you could easily +need this functionality in the real world, so here you go. +::: diff --git a/docs/guide/messaging/transports/signalr.md b/docs/guide/messaging/transports/signalr.md index c22edb785..1aecce433 100644 --- a/docs/guide/messaging/transports/signalr.md +++ b/docs/guide/messaging/transports/signalr.md @@ -40,7 +40,12 @@ builder.UseWolverine(opts => // to wire SignalR services into Wolverine itself // This does also call IServiceCollection.AddSignalR() // to register DI services for SignalR as well - opts.UseSignalR(); + opts.UseSignalR(o => + { + // Optionally configure the SignalR HubOptions + // for the WolverineHub + o.ClientTimeoutInterval = 10.Seconds(); + }); // Using explicit routing to send specific // messages to SignalR @@ -54,7 +59,7 @@ builder.UseWolverine(opts => }); }); ``` -snippet source | anchor +snippet source | anchor That handles the Wolverine configuration and the SignalR service registrations, but you will also need to map @@ -79,7 +84,7 @@ app.MapWolverineSignalRHub("/api/messages"); return await app.RunJasperFxCommands(args); ``` -snippet source | anchor +snippet source | anchor ## Messages and Serialization @@ -138,6 +143,20 @@ the raw message being sent to the client and received from the browser. Here's a } ``` +You can always preview the message type name by using the `dotnet run -- describe` command and finding the +"Message Routing" table in that output, which should look like this from the sample application: + +```text + Message Routing +┌───────────────────────────────┬────────────────────┬──────────────────────┬──────────────────┐ +│ .NET Type │ Message Type Alias │ Destination │ Content Type │ +├───────────────────────────────┼────────────────────┼──────────────────────┼──────────────────┤ +│ WolverineChat.ChatMessage │ chat_message │ signalr://wolverine/ │ application/json │ +│ WolverineChat.Ping │ ping │ signalr://wolverine/ │ application/json │ +│ WolverineChat.ResponseMessage │ response_message │ signalr://wolverine/ │ application/json │ +└───────────────────────────────┴────────────────────┴──────────────────────┴──────────────────┘ +``` + The only elements that are mandatory are the `type` node that should be the Wolverine message type name and `data` that is the actual message serialized by JSON. Wolverine will send the full CloudEvents envelope structure because it's reusing the envelope mapping from [our CloudEvents interoperability](/tutorials/interop.html#interop-with-cloudevents), but the browser code **only** needs to send `type` @@ -543,5 +562,3 @@ response back to the originating caller even if the work required intermediate s behavior today is the usage of the `[EnlistInCurrentConnectionSaga]` that should be on either - - diff --git a/docs/guide/migration.md b/docs/guide/migration.md index 7e5dedd56..9c9ece91c 100644 --- a/docs/guide/migration.md +++ b/docs/guide/migration.md @@ -1,8 +1,29 @@ # Migration Guide -## Key Changes in 4.0 +## Key Changes in 5.0 + +5.0 had very few breaking changes in the public API, but some in "publinternals" types most users would never touch. The +biggest change in the internals is the replacement of the venerable [TPL DataFlow library](https://learn.microsoft.com/en-us/dotnet/standard/parallel-programming/dataflow-task-parallel-library) +with the [System.Threading.Channels library](https://learn.microsoft.com/en-us/dotnet/core/extensions/channels) +in every place that Wolverine uses in memory queueing. The only change this caused to the public API was the removal of +the option for direct configuration of the TPL DataFlow `ExecutionOptions`. Endpoint ordering and parallelization options +are unchanged otherwise in the public fluent interface for configuration. + +The `IntegrateWithWolverine()` syntax for ["ancillary stores"](/guide/durability/marten/ancillary-stores) changed to a [nested closure](https://martinfowler.com/dslCatalog/nestedClosure.html) syntax to be more consistent +with the syntax for the main [Marten](https://martendb.io) store. The [Wolverine managed distribution of Marten projections and subscriptions](/guide/durability/marten/distribution) +now applies to the ancillary stores as well. + +The new [Partitioned Sequential Messaging](/guide/messaging/partitioning) feature is a potentially huge step forward for +building a Wolverine system that can efficiently and resiliently handle concurrent access to sensitive resources. -4.0 had very few breaking changes. +The [Aggregate Handler Workflow](/guide/durability/marten/event-sourcing) feature with Marten now supports strong typed identifiers. + +The declarative data access features with Marten (`[Aggregate]`, `[ReadAggregate]`, `[Entity]` or `[Document]`) can utilize +Marten batch querying for better efficiency when a handler or HTTP endpoint uses more than one declaration for data loading. + +Better control over how [Wolverine generates code with respect to IoC container usage](/guide/codegen.html#wolverine-code-generation-and-ioc). + +## Key Changes in 4.0 * Wolverine dropped all support for .NET 6/7 * The previous dependencies on Oakton, JasperFx.Core, and JasperFx.CodeGeneration were all combined into a single [JasperFx](https://github.com/jasperfx/jasperfx) library. There are shims for any method with "Oakton" in its name, but these are marked as `[Obsolete]`. You can pretty well do a find and replace for "Oakton" to "JasperFx". If your Oakton command classes live in a different project than the runnable application, add this to that project's `Properties/AssemblyInfo.cs` file: diff --git a/docs/guide/testing.md b/docs/guide/testing.md index 88c04b355..38425bd0b 100644 --- a/docs/guide/testing.md +++ b/docs/guide/testing.md @@ -82,7 +82,7 @@ public async Task using_tracked_sessions() overdrawn.AccountId.ShouldBe(debitAccount.AccountId); } ``` -snippet source | anchor +snippet source | anchor The tracked session mechanism utilizes Wolverine's internal instrumentation to "know" when all the outstanding @@ -141,6 +141,15 @@ public async Task using_tracked_sessions_advanced(IHost otherWolverineSystem) // Again, this is testing against processes, with another IHost .WaitForMessageToBeReceivedAt(otherWolverineSystem) + + // Wolverine does this automatically, but it's sometimes + // helpful to tell Wolverine to not track certain message + // types during testing. Especially messages originating from + // some kind of polling operation + .IgnoreMessageType() + + // Another option + .IgnoreMessagesMatchingType(type => type.CanBeCastTo()) // There are many other options as well .InvokeMessageAndWaitAsync(debitAccount); @@ -149,7 +158,7 @@ public async Task using_tracked_sessions_advanced(IHost otherWolverineSystem) overdrawn.AccountId.ShouldBe(debitAccount.AccountId); } ``` -snippet source | anchor +snippet source | anchor The samples shown above inlcude `Sent` message records, but there are more properties available in the `TrackedSession` object. @@ -289,7 +298,7 @@ public class When_message_is_sent : IAsyncLifetime public async Task DisposeAsync() => await _host.StopAsync(); } ``` -snippet source | anchor +snippet source | anchor As you can see, we just have to start our application, attach a tracked session to it, and then wait for the message to be published. This way, we can test the whole process of the application, from the file change to the message publication, in a single test. @@ -433,7 +442,7 @@ public static IEnumerable Handle( yield return new AccountUpdated(account.Id, account.Balance); } ``` -snippet source | anchor +snippet source | anchor The testing extensions can be seen in action by the following test: @@ -476,7 +485,7 @@ public void handle_a_debit_that_makes_the_account_have_a_low_balance() messages.ShouldHaveNoMessageOfType(); } ``` -snippet source | anchor +snippet source | anchor The supported extension methods so far are in the [TestingExtensions](https://github.com/JasperFx/wolverine/blob/main/src/Wolverine/TestingExtensions.cs) class. @@ -686,7 +695,7 @@ builder.UseWolverine(opts => using var host = builder.Build(); await host.StartAsync(); ``` -snippet source | anchor +snippet source | anchor I'm not necessarily comfortable with a lot of conditional hosting setup all the time, diff --git a/docs/tutorials/concurrency.md b/docs/tutorials/concurrency.md index 65ef46662..41d0ca806 100644 --- a/docs/tutorials/concurrency.md +++ b/docs/tutorials/concurrency.md @@ -137,6 +137,6 @@ in other nodes. More likely though, to both protect against concurrent access against resources that are prone to issues with concurrent access *and* allow for greater throughput, you may want to reach for either: -* Session Identifier and FIFO queue support for Azure Service Bus -* Wolverine's [Partitioned Sequential Messaging](https://wolverinefx.net/guide/messaging/transports/azureservicebus/session-identifiers.html) feature introduced in Wolverine 5.0 that was designed specifically to alleviate problems with concurrency within +* [Session Identifier and FIFO queue support for Azure Service Bus](/guide/messaging/transports/azureservicebus/session-identifiers) +* Wolverine's [Partitioned Sequential Messaging](/guide/messaging/partitioning) feature introduced in Wolverine 5.0 that was designed specifically to alleviate problems with concurrency within Wolverine systems. diff --git a/src/Http/Wolverine.Http.Tests/CodeGeneration/service_location_assertions.cs b/src/Http/Wolverine.Http.Tests/CodeGeneration/service_location_assertions.cs index 04f2c05b4..f04467d04 100644 --- a/src/Http/Wolverine.Http.Tests/CodeGeneration/service_location_assertions.cs +++ b/src/Http/Wolverine.Http.Tests/CodeGeneration/service_location_assertions.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using NSubstitute; using Shouldly; @@ -37,6 +38,25 @@ private IServiceProvider servicesWithPolicy(ServiceLocationPolicy policy) return services.BuildServiceProvider(); } + public interface IServiceGatewayUsingRefit; + + public static void configure_with_always_use_service_locator() + { + #region sample_always_use_service_location + + var builder = Host.CreateApplicationBuilder(); + builder.UseWolverine(opts => + { + // other configuration + + // Use a service locator for this service w/o forcing the entire + // message handler adapter to use a service locator for everything + opts.CodeGeneration.AlwaysUseServiceLocationFor(); + }); + + #endregion + } + private async Task buildHost(ServiceProviderSource providerSource, Action configure) { var builder = WebApplication.CreateBuilder([]); @@ -341,4 +361,74 @@ public record RedFlag : IFlag; public record GreenFlag : IFlag; public interface IGateway; -public class Gateway : IGateway; \ No newline at end of file +public class Gateway : IGateway; + +public interface IUserContext +{ + public string UserId { get;} +} + +public class UserContext : IUserContext +{ + public string UserId { get; set; } +} + +public class UserContextFactory +{ + public IUserContext Build(HttpContext context) => new UserContext(); +} + +public class MyCustomUserMiddleware(RequestDelegate next) +{ + private readonly RequestDelegate _next = next; + + public async Task InvokeAsync(HttpContext httpContext) + { + // and whatever else + await _next(httpContext); + } +} + +public static class SampleServiceLocation +{ + public static async Task bootstrap(string[] args) + { + #region sample_bootstrapping_with_httpcontext_request_services + + var builder = WebApplication.CreateBuilder(); + + builder.UseWolverine(opts => + { + // more configuration + }); + + // Just pretend that this IUserContext is being + builder.Services.AddScoped(); + builder.Services.AddWolverineHttp(); + + var app = builder.Build(); + + // Custom middleware that is somehow configuring our IUserContext + // that might be getting used within + app.UseMiddleware(); + + app.MapWolverineEndpoints(opts => + { + // Opt into using the shared HttpContext.RequestServices scoped + // container any time Wolverine has to use a service locator + opts.ServiceProviderSource = ServiceProviderSource.FromHttpContextRequestServices; + + // OR this is the default behavior to be backwards compatible: + opts.ServiceProviderSource = ServiceProviderSource.IsolatedAndScoped; + + // We're telling Wolverine that the IUserContext should always + // be pulled from HttpContext.RequestServices + // and this happens regardless of the ServerProviderSource! + opts.SourceServiceFromHttpContext(); + }); + + return await app.RunJasperFxCommands(args); + + #endregion + } +} \ No newline at end of file diff --git a/src/Samples/DocumentationSamples/ServiceLocationUsage.cs b/src/Samples/DocumentationSamples/ServiceLocationUsage.cs new file mode 100644 index 000000000..692c3994e --- /dev/null +++ b/src/Samples/DocumentationSamples/ServiceLocationUsage.cs @@ -0,0 +1,35 @@ +using JasperFx.CodeGeneration.Model; +using Microsoft.Extensions.Hosting; +using Wolverine; + +namespace DocumentationSamples; + +public class ServiceLocationUsage +{ + public static void configure() + { + #region sample_configuring_ServiceLocationPolicy + + var builder = Host.CreateApplicationBuilder(); + builder.UseWolverine(opts => + { + // This is the default behavior. Wolverine will allow you to utilize + // service location in the codegen, but will warn you through log messages + // when this happens + opts.ServiceLocationPolicy = ServiceLocationPolicy.AllowedButWarn; + + // Tell Wolverine to just be quiet about service location and let it + // all go. For any of you with small children, I defy you to get the + // Frozen song out of your head now... + opts.ServiceLocationPolicy = ServiceLocationPolicy.AlwaysAllowed; + + // Wolverine will throw exceptions at runtime if it encounters + // a message handler or HTTP endpoint that would require service + // location in the code generation + // Use this option to disallow any undesirably service location + opts.ServiceLocationPolicy = ServiceLocationPolicy.NotAllowed; + }); + + #endregion + } +} \ No newline at end of file diff --git a/src/Samples/DocumentationSamples/TestingSupportSamples.cs b/src/Samples/DocumentationSamples/TestingSupportSamples.cs index b33549fbf..79aefeb1e 100644 --- a/src/Samples/DocumentationSamples/TestingSupportSamples.cs +++ b/src/Samples/DocumentationSamples/TestingSupportSamples.cs @@ -1,4 +1,5 @@ using JasperFx.Core; +using JasperFx.Core.Reflection; using Marten; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -6,6 +7,7 @@ using Shouldly; using Wolverine; using Wolverine.Attributes; +using Wolverine.Runtime.Agents; using Wolverine.Tracking; using Xunit; @@ -172,6 +174,15 @@ public async Task using_tracked_sessions_advanced(IHost otherWolverineSystem) // Again, this is testing against processes, with another IHost .WaitForMessageToBeReceivedAt(otherWolverineSystem) + + // Wolverine does this automatically, but it's sometimes + // helpful to tell Wolverine to not track certain message + // types during testing. Especially messages originating from + // some kind of polling operation + .IgnoreMessageType() + + // Another option + .IgnoreMessagesMatchingType(type => type.CanBeCastTo()) // There are many other options as well .InvokeMessageAndWaitAsync(debitAccount); diff --git a/src/Samples/WolverineChat/Program.cs b/src/Samples/WolverineChat/Program.cs index 107c4fc58..366a679ad 100644 --- a/src/Samples/WolverineChat/Program.cs +++ b/src/Samples/WolverineChat/Program.cs @@ -1,4 +1,5 @@ using JasperFx; +using JasperFx.Core; using Wolverine; using Wolverine.SignalR; using WolverineChat; @@ -16,7 +17,12 @@ // to wire SignalR services into Wolverine itself // This does also call IServiceCollection.AddSignalR() // to register DI services for SignalR as well - opts.UseSignalR(); + opts.UseSignalR(o => + { + // Optionally configure the SignalR HubOptions + // for the WolverineHub + o.ClientTimeoutInterval = 10.Seconds(); + }); // Using explicit routing to send specific // messages to SignalR diff --git a/src/Transports/SignalR/Wolverine.SignalR/SignalRWolverineExtensions.cs b/src/Transports/SignalR/Wolverine.SignalR/SignalRWolverineExtensions.cs index 30d430eeb..17ef373b3 100644 --- a/src/Transports/SignalR/Wolverine.SignalR/SignalRWolverineExtensions.cs +++ b/src/Transports/SignalR/Wolverine.SignalR/SignalRWolverineExtensions.cs @@ -1,6 +1,7 @@ using JasperFx.Core.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; using Wolverine.Configuration; using Wolverine.Runtime; @@ -42,9 +43,23 @@ internal static SignalRTransport SignalRTransport(this WolverineOptions endpoint public static SignalRMessage ToWebSocketGroup(this T Message, string GroupName) => new(Message, new WebSocketRouting.Group(GroupName)); - public static SignalRListenerConfiguration UseSignalR(this WolverineOptions options) + /// + /// Adds the WolverineHub to this application for SignalR message processing + /// + /// + /// Optionally configure the SignalR HubOptions for Wolverine + /// + public static SignalRListenerConfiguration UseSignalR(this WolverineOptions options, Action? configure = null) { - options.Services.AddSignalR(); + if (configure == null) + { + options.Services.AddSignalR(); + } + else + { + options.Services.AddSignalR(configure); + } + var transport = options.SignalRTransport(); diff --git a/src/Wolverine/Runtime/Interop/CloudEventsMapper.cs b/src/Wolverine/Runtime/Interop/CloudEventsMapper.cs index ea4949f48..24f82c81a 100644 --- a/src/Wolverine/Runtime/Interop/CloudEventsMapper.cs +++ b/src/Wolverine/Runtime/Interop/CloudEventsMapper.cs @@ -8,6 +8,13 @@ namespace Wolverine.Runtime.Interop; +public class UnknownMessageTypeNameException : Exception +{ + public UnknownMessageTypeNameException(string? message) : base(message) + { + } +} + internal class CloudEventsEnvelope { public CloudEventsEnvelope() @@ -147,6 +154,10 @@ public void MapIncoming(Envelope envelope, JsonNode? node) envelope.MessageType = messageType.ToMessageTypeName(); } + else + { + throw new UnknownMessageTypeNameException($"Unknown message type alias '{cloudEventType}'. See the 'Message Routing' section of the dotnet run describe output to see the available .NET message types and their message type names"); + } } if (node.TryGetValue("datacontenttype", out var contentType)) diff --git a/src/Wolverine/WolverineSystemPart.cs b/src/Wolverine/WolverineSystemPart.cs index bb06dea89..9b20473e8 100644 --- a/src/Wolverine/WolverineSystemPart.cs +++ b/src/Wolverine/WolverineSystemPart.cs @@ -1,3 +1,4 @@ +using System.Reflection; using JasperFx.CommandLine.Descriptions; using JasperFx.Core.Reflection; using JasperFx.Resources; @@ -10,6 +11,7 @@ using Wolverine.Runtime; using Wolverine.Runtime.Routing; using Wolverine.Transports.Local; +using Wolverine.Util; namespace Wolverine; @@ -50,19 +52,32 @@ public void WriteMessageSubscriptions() if (!messageTypes.Any()) { - AnsiConsole.Markup("[gray]No message routes[/]"); + AnsiConsole.Markup("[gray]No message types found"); return; } - var table = new Table(){Title = new TableTitle("Message Routing"){Style = new Style(decoration:Decoration.Bold)}}.AddColumns("Message Type", "Destination", "Content Type"); - foreach (var messageType in messageTypes.OrderBy(x => x.FullName)) + var table = new Table(){Title = new TableTitle("Message Routing") + { + Style = new Style(decoration:Decoration.Bold) + }}.AddColumns(".NET Type", "Message Type Alias", "Destination", "Content Type"); + foreach (var messageType in messageTypes.Where(x => x.Assembly != Assembly.GetExecutingAssembly()).OrderBy(x => x.FullName)) { var routes = _runtime.RoutingFor(messageType).Routes; - foreach (var route in routes.OfType()) + if (routes.Any()) { - table.AddRow(messageType.FullNameInCode(), route.Uri.ToString(), - route.Serializer?.ContentType ?? "application/json"); + foreach (var route in routes.OfType()) + { + table.AddRow(messageType.FullNameInCode(), messageType.ToMessageTypeName(), route.Uri.ToString(), + route.Serializer?.ContentType ?? "application/json"); + } } + else + { + table.AddRow(messageType.FullNameInCode(), messageType.ToMessageTypeName(), "No Routes", + "n/a"); + } + + } AnsiConsole.Write(table);