diff --git a/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Data/InventoryDbContext.cs b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Data/InventoryDbContext.cs new file mode 100644 index 00000000..baa8506f --- /dev/null +++ b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Data/InventoryDbContext.cs @@ -0,0 +1,18 @@ +using Examples.Bootstrapping.MultiModule.Domain.Inventory; +using Microsoft.EntityFrameworkCore; +using RCommon.Persistence.EFCore; + +namespace Examples.Bootstrapping.MultiModule.Data; + +/// +/// DbContext for the Inventory module. A separate type from so that +/// both can coexist under distinct data-store names without conflicting. +/// +public class InventoryDbContext : RCommonDbContext +{ + public InventoryDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Items => Set(); +} diff --git a/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Data/OrderingDbContext.cs b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Data/OrderingDbContext.cs new file mode 100644 index 00000000..1928e8b1 --- /dev/null +++ b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Data/OrderingDbContext.cs @@ -0,0 +1,18 @@ +using Examples.Bootstrapping.MultiModule.Domain.Orders; +using Microsoft.EntityFrameworkCore; +using RCommon.Persistence.EFCore; + +namespace Examples.Bootstrapping.MultiModule.Data; + +/// +/// DbContext for the Ordering module. Inherits directly from so that +/// DataStoreFactoryOptions.Register<RCommonDbContext, OrderingDbContext>("Ordering") wires up correctly. +/// +public class OrderingDbContext : RCommonDbContext +{ + public OrderingDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Orders => Set(); +} diff --git a/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Domain/Inventory/InventoryItem.cs b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Domain/Inventory/InventoryItem.cs new file mode 100644 index 00000000..8f76869c --- /dev/null +++ b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Domain/Inventory/InventoryItem.cs @@ -0,0 +1,13 @@ +namespace Examples.Bootstrapping.MultiModule.Domain.Inventory; + +/// +/// Minimal InventoryItem aggregate used by the Inventory module. +/// +public class InventoryItem +{ + public Guid Id { get; set; } + + public string Sku { get; set; } = string.Empty; + + public int Quantity { get; set; } +} diff --git a/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Domain/Orders/Order.cs b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Domain/Orders/Order.cs new file mode 100644 index 00000000..d34ec7ce --- /dev/null +++ b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Domain/Orders/Order.cs @@ -0,0 +1,12 @@ +namespace Examples.Bootstrapping.MultiModule.Domain.Orders; + +/// +/// Minimal Order aggregate used by the Ordering module. Persistence configuration is +/// intentionally lightweight; the example focuses on bootstrapping semantics, not domain depth. +/// +public class Order +{ + public Guid Id { get; set; } + + public string CustomerName { get; set; } = string.Empty; +} diff --git a/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Examples.Bootstrapping.MultiModule.csproj b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Examples.Bootstrapping.MultiModule.csproj new file mode 100644 index 00000000..fac1f5e9 --- /dev/null +++ b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Examples.Bootstrapping.MultiModule.csproj @@ -0,0 +1,23 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Modules/IServiceModule.cs b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Modules/IServiceModule.cs new file mode 100644 index 00000000..be2416cb --- /dev/null +++ b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Modules/IServiceModule.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Examples.Bootstrapping.MultiModule.Modules; + +/// +/// Minimal contract a module implements to configure services in a multi-module composition. +/// Each module independently calls services.AddRCommon(); the bootstrapper guarantees +/// idempotent and merge-able registration semantics across modules. +/// +public interface IServiceModule +{ + void Configure(IServiceCollection services); +} diff --git a/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Modules/InventoryModule.cs b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Modules/InventoryModule.cs new file mode 100644 index 00000000..8976f056 --- /dev/null +++ b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Modules/InventoryModule.cs @@ -0,0 +1,26 @@ +using Examples.Bootstrapping.MultiModule.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using RCommon; +using RCommon.Caching; +using RCommon.MemoryCache; + +namespace Examples.Bootstrapping.MultiModule.Modules; + +/// +/// The Inventory module configures its own DbContext under a distinct data-store name, +/// re-declares the same guid generator (idempotent no-op), and adds in-memory caching. +/// +public class InventoryModule : IServiceModule +{ + public void Configure(IServiceCollection services) + { + services.AddRCommon() + .WithSimpleGuidGenerator() // Same impl as Ordering -> idempotent no-op. + .WithPersistence(ef => + ef.AddDbContext( + "Inventory", + o => o.UseInMemoryDatabase("inventory"))) + .WithMemoryCaching(); + } +} diff --git a/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Modules/NotificationsModule.cs b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Modules/NotificationsModule.cs new file mode 100644 index 00000000..12860302 --- /dev/null +++ b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Modules/NotificationsModule.cs @@ -0,0 +1,20 @@ +using Examples.Bootstrapping.MultiModule.Producers; +using Microsoft.Extensions.DependencyInjection; +using RCommon; +using RCommon.EventHandling; + +namespace Examples.Bootstrapping.MultiModule.Modules; + +/// +/// The Notifications module re-registers (already registered by +/// ). The bootstrapper detects the duplicate and keeps a single +/// descriptor, so IServiceProvider.GetServices<IEventProducer>() resolves it once. +/// +public class NotificationsModule : IServiceModule +{ + public void Configure(IServiceCollection services) + { + services.AddRCommon() + .WithEventHandling(eh => eh.AddProducer()); + } +} diff --git a/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Modules/OrderingModule.cs b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Modules/OrderingModule.cs new file mode 100644 index 00000000..669554aa --- /dev/null +++ b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Modules/OrderingModule.cs @@ -0,0 +1,26 @@ +using Examples.Bootstrapping.MultiModule.Data; +using Examples.Bootstrapping.MultiModule.Producers; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using RCommon; +using RCommon.EventHandling; + +namespace Examples.Bootstrapping.MultiModule.Modules; + +/// +/// The Ordering module configures persistence for its own DbContext and a guid generator, +/// and registers an for cross-cutting auditing. +/// +public class OrderingModule : IServiceModule +{ + public void Configure(IServiceCollection services) + { + services.AddRCommon() + .WithSimpleGuidGenerator() // Singleton verb: idempotent across modules when impl matches. + .WithPersistence(ef => + ef.AddDbContext( + "Ordering", + o => o.UseInMemoryDatabase("ordering"))) + .WithEventHandling(eh => eh.AddProducer()); + } +} diff --git a/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Producers/AuditProducer.cs b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Producers/AuditProducer.cs new file mode 100644 index 00000000..56c1d4f4 --- /dev/null +++ b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Producers/AuditProducer.cs @@ -0,0 +1,18 @@ +using RCommon.EventHandling.Producers; +using RCommon.Models.Events; + +namespace Examples.Bootstrapping.MultiModule.Producers; + +/// +/// No-op used to demonstrate cross-module producer dedup. +/// Two modules register this same producer type; the bootstrapper merges them into a single descriptor. +/// +public class AuditProducer : IEventProducer +{ + public Task ProduceEventAsync(TEvent @event, CancellationToken cancellationToken = default) + where TEvent : ISerializableEvent + { + // No-op for example purposes. + return Task.CompletedTask; + } +} diff --git a/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Program.cs b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Program.cs new file mode 100644 index 00000000..7ad275df --- /dev/null +++ b/Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/Program.cs @@ -0,0 +1,46 @@ +using Examples.Bootstrapping.MultiModule.Modules; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using RCommon; +using RCommon.EventHandling.Producers; + +var host = Host.CreateDefaultBuilder(args) + .ConfigureServices(services => + { + IServiceModule[] modules = + { + new OrderingModule(), + new InventoryModule(), + new NotificationsModule(), + }; + + Console.WriteLine($"IsRCommonInitialized before any module: {services.IsRCommonInitialized()}"); + + foreach (var module in modules) + { + module.Configure(services); + Console.WriteLine($" {module.GetType().Name} configured; IsRCommonInitialized={services.IsRCommonInitialized()}"); + } + }) + .Build(); + +await host.StartAsync(); + +var sp = host.Services; +var producerCount = sp.GetServices().Count(); +Console.WriteLine($"\nDistinct IEventProducer instances resolved: {producerCount}"); +Console.WriteLine("Expected: 1 (AuditProducer was registered by Ordering and Notifications but dedup keeps it to one).\n"); + +var builder = sp.GetRequiredService(); +var diagnostics = builder.GetBootstrapDiagnostics(); +if (!string.IsNullOrEmpty(diagnostics)) +{ + Console.WriteLine("Bootstrap diagnostics report:"); + Console.WriteLine(diagnostics); +} +else +{ + Console.WriteLine("Bootstrap diagnostics: no soft duplicates detected."); +} + +await host.StopAsync(); diff --git a/Examples/Examples.sln b/Examples/Examples.sln index 13afa34a..ca5f162b 100644 --- a/Examples/Examples.sln +++ b/Examples/Examples.sln @@ -121,6 +121,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RCommon.TestBase", "..\Test EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples.Messaging.SubscriptionIsolation", "Messaging\Examples.Messaging.SubscriptionIsolation\Examples.Messaging.SubscriptionIsolation.csproj", "{DFF5FC1F-D20F-4BFE-BD58-A0B4F1E414CB}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Bootstrapping", "Bootstrapping", "{173CBA2D-76A2-457F-947A-8CBE38D63121}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.Bootstrapping.MultiModule", "Bootstrapping\Examples.Bootstrapping.MultiModule\Examples.Bootstrapping.MultiModule.csproj", "{60782FC7-AB13-44D9-A4F1-E8C602697C3E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -719,6 +723,18 @@ Global {DFF5FC1F-D20F-4BFE-BD58-A0B4F1E414CB}.Release|x64.Build.0 = Release|Any CPU {DFF5FC1F-D20F-4BFE-BD58-A0B4F1E414CB}.Release|x86.ActiveCfg = Release|Any CPU {DFF5FC1F-D20F-4BFE-BD58-A0B4F1E414CB}.Release|x86.Build.0 = Release|Any CPU + {60782FC7-AB13-44D9-A4F1-E8C602697C3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {60782FC7-AB13-44D9-A4F1-E8C602697C3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {60782FC7-AB13-44D9-A4F1-E8C602697C3E}.Debug|x64.ActiveCfg = Debug|Any CPU + {60782FC7-AB13-44D9-A4F1-E8C602697C3E}.Debug|x64.Build.0 = Debug|Any CPU + {60782FC7-AB13-44D9-A4F1-E8C602697C3E}.Debug|x86.ActiveCfg = Debug|Any CPU + {60782FC7-AB13-44D9-A4F1-E8C602697C3E}.Debug|x86.Build.0 = Debug|Any CPU + {60782FC7-AB13-44D9-A4F1-E8C602697C3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {60782FC7-AB13-44D9-A4F1-E8C602697C3E}.Release|Any CPU.Build.0 = Release|Any CPU + {60782FC7-AB13-44D9-A4F1-E8C602697C3E}.Release|x64.ActiveCfg = Release|Any CPU + {60782FC7-AB13-44D9-A4F1-E8C602697C3E}.Release|x64.Build.0 = Release|Any CPU + {60782FC7-AB13-44D9-A4F1-E8C602697C3E}.Release|x86.ActiveCfg = Release|Any CPU + {60782FC7-AB13-44D9-A4F1-E8C602697C3E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -768,6 +784,8 @@ Global {43AD4063-F157-994B-5A6D-92B9ABCA126E} = {3234C3BB-1632-4684-838E-9D6D382D4D4D} {740F4266-7ADA-7893-C575-B8C531C029EC} = {3234C3BB-1632-4684-838E-9D6D382D4D4D} {DFF5FC1F-D20F-4BFE-BD58-A0B4F1E414CB} = {35AE0870-0A6D-4F27-B534-B8DCDFD11A36} + {173CBA2D-76A2-457F-947A-8CBE38D63121} = {3234C3BB-1632-4684-838E-9D6D382D4D4D} + {60782FC7-AB13-44D9-A4F1-E8C602697C3E} = {173CBA2D-76A2-457F-947A-8CBE38D63121} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0B0CD26D-8067-4667-863E-6B0EE7EDAA42} diff --git a/Src/RCommon.ApplicationServices/CqrsBuilderExtensions.cs b/Src/RCommon.ApplicationServices/CqrsBuilderExtensions.cs index 33e5e0c2..e6ab021a 100644 --- a/Src/RCommon.ApplicationServices/CqrsBuilderExtensions.cs +++ b/Src/RCommon.ApplicationServices/CqrsBuilderExtensions.cs @@ -50,7 +50,7 @@ public static class CqrsBuilderExtensions /// The RCommon builder. /// The for further chaining. public static IRCommonBuilder WithCQRS(this IRCommonBuilder builder) - where T : ICqrsBuilder + where T : class, ICqrsBuilder { return WithCQRS(builder, x => { }); @@ -64,11 +64,13 @@ public static IRCommonBuilder WithCQRS(this IRCommonBuilder builder) /// A delegate to configure the CQRS builder (e.g., register handlers). /// The for further chaining. public static IRCommonBuilder WithCQRS(this IRCommonBuilder builder, Action actions) - where T : ICqrsBuilder + where T : class, ICqrsBuilder { - // Instantiate the CQRS builder implementation, which registers core bus services in its constructor - var cqrsBuilder = (T)Activator.CreateInstance(typeof(T), new object[] { builder })!; + // Instantiate the CQRS builder implementation, which registers core bus services in its constructor. + // Routed through GetOrAddBuilder so repeated WithCQRS calls reuse the cached sub-builder. + var cqrsBuilder = builder.GetOrAddBuilder( + () => (T)Activator.CreateInstance(typeof(T), new object[] { builder })!); actions(cqrsBuilder); return builder; } diff --git a/Src/RCommon.ApplicationServices/ValidationBuilderExtensions.cs b/Src/RCommon.ApplicationServices/ValidationBuilderExtensions.cs index df50f837..b72e241e 100644 --- a/Src/RCommon.ApplicationServices/ValidationBuilderExtensions.cs +++ b/Src/RCommon.ApplicationServices/ValidationBuilderExtensions.cs @@ -23,7 +23,7 @@ public static class ValidationBuilderExtensions /// The RCommon builder. /// The for further chaining. public static IRCommonBuilder WithValidation(this IRCommonBuilder builder) - where T : IValidationBuilder + where T : class, IValidationBuilder { return WithValidation(builder, x => { }); } @@ -36,14 +36,16 @@ public static IRCommonBuilder WithValidation(this IRCommonBuilder builder) /// A delegate to configure the validation builder (e.g., register validation providers). /// The for further chaining. public static IRCommonBuilder WithValidation(this IRCommonBuilder builder, Action actions) - where T : IValidationBuilder + where T : class, IValidationBuilder { builder.Services.AddScoped(); - // Instantiate the validation builder implementation, which may register provider-specific services - var mediatorConfig = (T)Activator.CreateInstance(typeof(T), new object[] { builder })!; - actions(mediatorConfig); + // Instantiate the validation builder implementation, which may register provider-specific services. + // Routed through GetOrAddBuilder so repeated WithValidation calls reuse the cached sub-builder. + var validationConfig = builder.GetOrAddBuilder( + () => (T)Activator.CreateInstance(typeof(T), new object[] { builder })!); + actions(validationConfig); return builder; } diff --git a/Src/RCommon.Blobs/BlobStorageBuilderExtensions.cs b/Src/RCommon.Blobs/BlobStorageBuilderExtensions.cs index 1bfe2f0e..bf95c300 100644 --- a/Src/RCommon.Blobs/BlobStorageBuilderExtensions.cs +++ b/Src/RCommon.Blobs/BlobStorageBuilderExtensions.cs @@ -12,7 +12,7 @@ public static class BlobStorageBuilderExtensions /// Registers a blob storage provider with default configuration. /// public static IRCommonBuilder WithBlobStorage(this IRCommonBuilder builder) - where T : IBlobStorageBuilder + where T : class, IBlobStorageBuilder { return WithBlobStorage(builder, x => { }); } @@ -22,14 +22,17 @@ public static IRCommonBuilder WithBlobStorage(this IRCommonBuilder builder) /// Can be called multiple times to register multiple providers (e.g. Azure + S3). /// public static IRCommonBuilder WithBlobStorage(this IRCommonBuilder builder, Action actions) - where T : IBlobStorageBuilder + where T : class, IBlobStorageBuilder { Guard.IsNotNull(actions, nameof(actions)); builder.Services.TryAddSingleton(); - var blobConfig = (T)(Activator.CreateInstance(typeof(T), new object[] { builder }) - ?? throw new InvalidOperationException($"Failed to create instance of {typeof(T).Name}.")); + // Routed through GetOrAddBuilder so repeated WithBlobStorage calls for the same provider type + // reuse the cached sub-builder. Different T (e.g. Azure vs S3) still get distinct sub-builders. + var blobConfig = builder.GetOrAddBuilder( + () => (T)(Activator.CreateInstance(typeof(T), new object[] { builder }) + ?? throw new InvalidOperationException($"Failed to create instance of {typeof(T).Name}."))); actions(blobConfig); return builder; } diff --git a/Src/RCommon.Caching/CachingBuilderExtensions.cs b/Src/RCommon.Caching/CachingBuilderExtensions.cs index ca5a4637..c7a2d606 100644 --- a/Src/RCommon.Caching/CachingBuilderExtensions.cs +++ b/Src/RCommon.Caching/CachingBuilderExtensions.cs @@ -20,7 +20,7 @@ public static class CachingBuilderExtensions /// The RCommon builder instance. /// The same for chaining. public static IRCommonBuilder WithMemoryCaching(this IRCommonBuilder builder) - where T : IMemoryCachingBuilder + where T : class, IMemoryCachingBuilder { return WithMemoryCaching(builder, x => { }); } @@ -37,12 +37,14 @@ public static IRCommonBuilder WithMemoryCaching(this IRCommonBuilder builder) /// and must have a constructor that accepts an . /// public static IRCommonBuilder WithMemoryCaching(this IRCommonBuilder builder, Action actions) - where T : IMemoryCachingBuilder + where T : class, IMemoryCachingBuilder { Guard.IsNotNull(actions, nameof(actions)); - // Create the builder via reflection, passing the IRCommonBuilder to its constructor - var cachingConfig = (T)(Activator.CreateInstance(typeof(T), new object[] { builder }) - ?? throw new InvalidOperationException($"Failed to create instance of {typeof(T).Name}.")); + // Create the builder via reflection, passing the IRCommonBuilder to its constructor. + // Routed through GetOrAddBuilder so repeated WithMemoryCaching calls reuse the cached sub-builder. + var cachingConfig = builder.GetOrAddBuilder( + () => (T)(Activator.CreateInstance(typeof(T), new object[] { builder }) + ?? throw new InvalidOperationException($"Failed to create instance of {typeof(T).Name}."))); actions(cachingConfig); return builder; } @@ -54,7 +56,7 @@ public static IRCommonBuilder WithMemoryCaching(this IRCommonBuilder builder, /// The RCommon builder instance. /// The same for chaining. public static IRCommonBuilder WithDistributedCaching(this IRCommonBuilder builder) - where T : IDistributedCachingBuilder + where T : class, IDistributedCachingBuilder { return WithDistributedCaching(builder, x => { }); } @@ -71,12 +73,14 @@ public static IRCommonBuilder WithDistributedCaching(this IRCommonBuilder bui /// and must have a constructor that accepts an . /// public static IRCommonBuilder WithDistributedCaching(this IRCommonBuilder builder, Action actions) - where T : IDistributedCachingBuilder + where T : class, IDistributedCachingBuilder { Guard.IsNotNull(actions, nameof(actions)); - // Create the builder via reflection, passing the IRCommonBuilder to its constructor - var cachingConfig = (T)(Activator.CreateInstance(typeof(T), new object[] { builder }) - ?? throw new InvalidOperationException($"Failed to create instance of {typeof(T).Name}.")); + // Create the builder via reflection, passing the IRCommonBuilder to its constructor. + // Routed through GetOrAddBuilder so repeated WithDistributedCaching calls reuse the cached sub-builder. + var cachingConfig = builder.GetOrAddBuilder( + () => (T)(Activator.CreateInstance(typeof(T), new object[] { builder }) + ?? throw new InvalidOperationException($"Failed to create instance of {typeof(T).Name}."))); actions(cachingConfig); return builder; } diff --git a/Src/RCommon.Core/Bootstrapping/RCommonBuilderInternals.cs b/Src/RCommon.Core/Bootstrapping/RCommonBuilderInternals.cs new file mode 100644 index 00000000..78d92f88 --- /dev/null +++ b/Src/RCommon.Core/Bootstrapping/RCommonBuilderInternals.cs @@ -0,0 +1,19 @@ +using System; + +namespace RCommon.Bootstrapping +{ + /// + /// Internal helpers for singleton-style WithX verbs that live outside RCommon.Core. + /// Not intended for use by application code. + /// + public static class RCommonBuilderInternals + { + /// + /// Returns the cached sub-builder concrete type that implements , or null if none. + /// + public static Type? FindCachedImplementationOf(IRCommonBuilder builder) + { + return (builder as RCommonBuilder)?.TryGetCachedSubBuilderImplementing(typeof(TInterface)); + } + } +} diff --git a/Src/RCommon.Core/EventHandling/EventHandlingBuilderExtensions.cs b/Src/RCommon.Core/EventHandling/EventHandlingBuilderExtensions.cs index 382899de..b34c0bc5 100644 --- a/Src/RCommon.Core/EventHandling/EventHandlingBuilderExtensions.cs +++ b/Src/RCommon.Core/EventHandling/EventHandlingBuilderExtensions.cs @@ -26,7 +26,7 @@ public static class EventHandlingBuilderExtensions /// The RCommon builder instance. /// The for method chaining. public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder) - where T : IEventHandlingBuilder + where T : class, IEventHandlingBuilder { return WithEventHandling(builder, x => { }); } @@ -39,10 +39,10 @@ public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder) /// An action to configure the event handling builder. /// The for method chaining. public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder, Action actions) - where T : IEventHandlingBuilder + where T : class, IEventHandlingBuilder { - // Event Handling Configurations - var eventHandlingConfig = (T)Activator.CreateInstance(typeof(T), new object[] { builder })!; + var eventHandlingConfig = builder.GetOrAddBuilder( + () => (T)Activator.CreateInstance(typeof(T), new object[] { builder })!); actions(eventHandlingConfig); return builder; } @@ -56,7 +56,12 @@ public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder, public static void AddProducer(this IEventHandlingBuilder builder) where T : class, IEventProducer { - builder.Services.AddSingleton(); + var alreadyRegistered = builder.Services.Any(d => + d.ServiceType == typeof(IEventProducer) && d.ImplementationType == typeof(T)); + if (!alreadyRegistered) + { + builder.Services.AddSingleton(); + } // Track which producer type is associated with this builder type var subscriptionManager = builder.Services.GetSubscriptionManager(); @@ -73,10 +78,17 @@ public static void AddProducer(this IEventHandlingBuilder builder) public static void AddProducer(this IEventHandlingBuilder builder, Func getProducer) where T : class, IEventProducer { - builder.Services.AddSingleton(getProducer); + // Factory descriptors set ImplementationFactory, not ImplementationType, so descriptor + // scanning won't reliably dedupe. Gate on the subscription manager's set-based tracking. + var subscriptionManager = builder.Services.GetSubscriptionManager(); + var alreadyTracked = subscriptionManager?.HasProducerForBuilder(builder.GetType(), typeof(T)) ?? false; + + if (!alreadyTracked) + { + builder.Services.AddSingleton(getProducer); + } // Track which producer type is associated with this builder type - var subscriptionManager = builder.Services.GetSubscriptionManager(); subscriptionManager?.AddProducerForBuilder(builder.GetType(), typeof(T)); } diff --git a/Src/RCommon.Core/EventHandling/Producers/EventSubscriptionManager.cs b/Src/RCommon.Core/EventHandling/Producers/EventSubscriptionManager.cs index b17b972e..7e6a694a 100644 --- a/Src/RCommon.Core/EventHandling/Producers/EventSubscriptionManager.cs +++ b/Src/RCommon.Core/EventHandling/Producers/EventSubscriptionManager.cs @@ -33,6 +33,21 @@ public void AddProducerForBuilder(Type builderType, Type producerType) } } + /// + /// Returns true if the given producer type has already been registered through the given builder type. + /// + public bool HasProducerForBuilder(Type builderType, Type producerType) + { + if (_builderProducerMap.TryGetValue(builderType, out var producers)) + { + lock (producers) + { + return producers.Contains(producerType); + } + } + return false; + } + /// /// Records that an event type should be handled by all producers registered on a specific builder. /// Called during AddSubscriber configuration. diff --git a/Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs b/Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs index 7eea61d0..50a39503 100644 --- a/Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs +++ b/Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs @@ -23,11 +23,35 @@ public static class ServiceCollectionExtensions /// RCommon configuration interface for additional chaining. public static IRCommonBuilder AddRCommon(this IServiceCollection services) { + var existing = services.FirstOrDefault(d => d.ServiceType == typeof(IRCommonBuilder)); + if (existing?.ImplementationInstance is IRCommonBuilder cached) + { + return cached; + } + var config = new RCommonBuilder(services); - config.Configure(); + services.AddSingleton(config); + config.Configure(); // No-op in the base class (just returns Services); preserved for consistency with the existing API shape and in case a subclass overrides it. + + // Register finalize diagnostics hosted service exactly once (first AddRCommon call only). + // The cached-return branch above ensures subsequent AddRCommon calls do not re-register. + services.AddSingleton(sp => + new RCommonBootstrapDiagnosticsHostedService( + services, + sp.GetRequiredService(), + sp.GetService())); + return config; } + /// + /// Returns true if has been invoked against this collection. + /// + public static bool IsRCommonInitialized(this IServiceCollection services) + { + return services.Any(d => d.ServiceType == typeof(IRCommonBuilder)); + } + /// /// Registers as an singleton if the type implements the interface. /// diff --git a/Src/RCommon.Core/IRCommonBuilder.cs b/Src/RCommon.Core/IRCommonBuilder.cs index 99c13a09..9bd697ff 100644 --- a/Src/RCommon.Core/IRCommonBuilder.cs +++ b/Src/RCommon.Core/IRCommonBuilder.cs @@ -53,5 +53,22 @@ public interface IRCommonBuilder IRCommonBuilder WithCommonFactory() where TService : class where TImplementation : class, TService; + + /// + /// Returns the cached sub-builder for if one exists, + /// otherwise invokes , caches the result, and returns it. + /// + /// Concrete sub-builder type (e.g., EFCorePerisistenceBuilder). + /// Parameterless factory invoked exactly once per + /// per . Callers close over whichever constructor argument the sub-builder + /// requires — typically builder.Services or builder itself. + TSubBuilder GetOrAddBuilder(Func factory) + where TSubBuilder : class; + + /// + /// Returns soft-duplicate diagnostic output stashed at finalize when no + /// was available. Empty string if no duplicates were detected. + /// + string GetBootstrapDiagnostics(); } } diff --git a/Src/RCommon.Core/RCommonBootstrapDiagnosticsHostedService.cs b/Src/RCommon.Core/RCommonBootstrapDiagnosticsHostedService.cs new file mode 100644 index 00000000..a7a31ca2 --- /dev/null +++ b/Src/RCommon.Core/RCommonBootstrapDiagnosticsHostedService.cs @@ -0,0 +1,55 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace RCommon +{ + /// + /// Runs the duplicate-registration scanner once at host startup and emits a single + /// warning (or stashes the message on the builder) if soft duplicates are detected. + /// + internal sealed class RCommonBootstrapDiagnosticsHostedService : IHostedService + { + private readonly IServiceCollection _services; + private readonly IRCommonBuilder _builder; + private readonly ILoggerFactory? _loggerFactory; + + public RCommonBootstrapDiagnosticsHostedService( + IServiceCollection services, + IRCommonBuilder builder, + ILoggerFactory? loggerFactory = null) + { + _services = services; + _builder = builder; + _loggerFactory = loggerFactory; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + if (_builder is not RCommonBuilder rb || !rb.TrySetDiagnosticsRun()) + { + return Task.CompletedTask; + } + + var report = _services.GeneratePossibleDuplicatesServiceDescriptorsString(); + if (string.IsNullOrWhiteSpace(report)) + { + return Task.CompletedTask; + } + + rb.StashDiagnostics(report); + + if (_loggerFactory is not null) + { + var logger = _loggerFactory.CreateLogger(); + logger.LogWarning("RCommon bootstrap detected duplicate service registrations:\n{Report}", report); + } + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} diff --git a/Src/RCommon.Core/RCommonBuilder.cs b/Src/RCommon.Core/RCommonBuilder.cs index 14e57c17..c2a739eb 100644 --- a/Src/RCommon.Core/RCommonBuilder.cs +++ b/Src/RCommon.Core/RCommonBuilder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using System.Reflection; using RCommon.EventHandling; @@ -15,8 +16,11 @@ public class RCommonBuilder : IRCommonBuilder /// public IServiceCollection Services { get; } - private bool _guidConfigured = false; - private bool _dateTimeConfigured = false; + private SingletonRegistration _guidRegistration; + private SingletonRegistration _dateTimeRegistration; + private readonly Dictionary _subBuilderCache = new(); + private bool _diagnosticsRun; + private string _bootstrapDiagnostics = string.Empty; /// /// Initializes a new instance of and registers core framework services @@ -44,37 +48,66 @@ public RCommonBuilder(IServiceCollection services) } /// - /// Thrown if a GUID generator has already been configured. + /// Thrown if a different GUID generator implementation has already been configured. public IRCommonBuilder WithSequentialGuidGenerator(Action actions) { - Guard.Against(this._guidConfigured, - "Guid Generator has already been configured once. You cannot configure multiple times"); + if (_guidRegistration.Configured) + { + if (_guidRegistration.ImplementationType == typeof(SequentialGuidGenerator)) + { + // Same impl re-registered: idempotent; just append the options delegate + this.Services.Configure(actions); + return this; + } + throw new RCommonBuilderException( + $"IGuidGenerator already configured as '{_guidRegistration.ImplementationType?.FullName}'; " + + $"cannot reconfigure as '{typeof(SequentialGuidGenerator).FullName}'. " + + "To configure multiple modules consistently, ensure all modules agree on the same IGuidGenerator implementation, " + + "or designate a single composition root that performs this registration."); + } + this.Services.Configure(actions); this.Services.AddTransient(); - this._guidConfigured = true; + _guidRegistration = new SingletonRegistration { Configured = true, ImplementationType = typeof(SequentialGuidGenerator) }; return this; } /// - /// Thrown if a GUID generator has already been configured. + /// Thrown if a different GUID generator implementation has already been configured. public IRCommonBuilder WithSimpleGuidGenerator() { - Guard.Against(this._guidConfigured, - "Guid Generator has already been configured once. You cannot configure multiple times"); + if (_guidRegistration.Configured) + { + if (_guidRegistration.ImplementationType == typeof(SimpleGuidGenerator)) + { + return this; + } + throw new RCommonBuilderException( + $"IGuidGenerator already configured as '{_guidRegistration.ImplementationType?.FullName}'; " + + $"cannot reconfigure as '{typeof(SimpleGuidGenerator).FullName}'. " + + "To configure multiple modules consistently, ensure all modules agree on the same IGuidGenerator implementation, " + + "or designate a single composition root that performs this registration."); + } + this.Services.AddScoped(); - this._guidConfigured = true; + _guidRegistration = new SingletonRegistration { Configured = true, ImplementationType = typeof(SimpleGuidGenerator) }; return this; } /// - /// Thrown if the date/time system has already been configured. public IRCommonBuilder WithDateTimeSystem(Action actions) { - Guard.Against(this._dateTimeConfigured, - "Date/Time System has already been configured once. You cannot configure multiple times"); + if (_dateTimeRegistration.Configured) + { + // Only one impl type exists; always idempotent. Still append the options delegate so + // additional configuration accumulates per Options pattern. + this.Services.Configure(actions); + return this; + } + this.Services.Configure(actions); this.Services.AddTransient(); - this._dateTimeConfigured = true; + _dateTimeRegistration = new SingletonRegistration { Configured = true, ImplementationType = typeof(SystemTime) }; return this; } @@ -94,5 +127,61 @@ public virtual IServiceCollection Configure() { return this.Services; } + + /// + public TSubBuilder GetOrAddBuilder(Func factory) + where TSubBuilder : class + { + if (_subBuilderCache.TryGetValue(typeof(TSubBuilder), out var cached)) + { + return (TSubBuilder)cached; + } + + var built = factory(); + _subBuilderCache[typeof(TSubBuilder)] = built; + return built; + } + + /// + /// Walks the sub-builder cache and returns the first cached concrete type that is assignable + /// to , or null if no such cached builder exists. + /// + /// + /// Used by singleton-style WithX verbs defined outside RCommon.Core to detect "different T" + /// configuration conflicts before delegating to . + /// + internal Type? TryGetCachedSubBuilderImplementing(Type interfaceType) + { + foreach (var key in _subBuilderCache.Keys) + { + if (interfaceType.IsAssignableFrom(key)) + { + return key; + } + } + return null; + } + + /// + /// Atomically marks the diagnostics scanner as having run. Returns true on first call, + /// false thereafter. Used by to + /// ensure the duplicate-registration scan executes at most once per builder instance. + /// + internal bool TrySetDiagnosticsRun() + { + if (_diagnosticsRun) return false; + _diagnosticsRun = true; + return true; + } + + /// + /// Stashes the soft-duplicate diagnostic report so it can be retrieved later via + /// when no + /// was available to log the warning directly. + /// + internal void StashDiagnostics(string message) => _bootstrapDiagnostics = message; + + /// + public string GetBootstrapDiagnostics() => _bootstrapDiagnostics; } } diff --git a/Src/RCommon.Core/SingletonRegistration.cs b/Src/RCommon.Core/SingletonRegistration.cs new file mode 100644 index 00000000..7b432cf8 --- /dev/null +++ b/Src/RCommon.Core/SingletonRegistration.cs @@ -0,0 +1,15 @@ +using System; + +namespace RCommon +{ + /// + /// Tracks whether a singleton-style RCommon verb has been configured and which implementation + /// type was chosen. Used by verbs like WithSimpleGuidGenerator and WithDateTimeSystem + /// to enforce same-type-idempotent / different-type-throw semantics across modular calls. + /// + internal struct SingletonRegistration + { + public bool Configured; + public Type? ImplementationType; + } +} diff --git a/Src/RCommon.Emailing/EmailingBuilderExtensions.cs b/Src/RCommon.Emailing/EmailingBuilderExtensions.cs index 4a31f700..461a3fde 100644 --- a/Src/RCommon.Emailing/EmailingBuilderExtensions.cs +++ b/Src/RCommon.Emailing/EmailingBuilderExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using RCommon.Emailing; using RCommon.Emailing.Smtp; using System; @@ -21,10 +22,23 @@ public static class EmailingBuilderExtensions /// The RCommon builder to configure. /// A delegate to configure . /// The same instance for fluent chaining. + /// + /// Thrown when has already been registered with a different + /// implementation type, since mixing email service implementations is unsupported. + /// public static IRCommonBuilder WithSmtpEmailServices(this IRCommonBuilder config, Action emailSettings) { + var existing = config.Services.FirstOrDefault(d => d.ServiceType == typeof(IEmailService)); + if (existing?.ImplementationType is { } existingImpl && existingImpl != typeof(SmtpEmailService)) + { + throw new RCommonBuilderException( + $"IEmailService already configured as '{existingImpl.FullName}'; cannot reconfigure as '{typeof(SmtpEmailService).FullName}'. " + + "To configure multiple modules consistently, ensure all modules agree on the same IEmailService implementation, " + + "or designate a single composition root that performs this registration."); + } + config.Services.Configure(emailSettings); - config.Services.AddTransient(); + config.Services.TryAddTransient(); return config; } } diff --git a/Src/RCommon.FluentValidation/ValidationBuilderExtensions.cs b/Src/RCommon.FluentValidation/ValidationBuilderExtensions.cs index 37cbc065..1f331b12 100644 --- a/Src/RCommon.FluentValidation/ValidationBuilderExtensions.cs +++ b/Src/RCommon.FluentValidation/ValidationBuilderExtensions.cs @@ -23,7 +23,7 @@ public static class ValidationBuilderExtensions /// The RCommon builder instance. /// The for further chaining. public static IRCommonBuilder WithValidation(this IRCommonBuilder builder) - where T : IValidationBuilder + where T : class, IValidationBuilder { return WithValidation(builder, x => { }); @@ -43,11 +43,13 @@ public static IRCommonBuilder WithValidation(this IRCommonBuilder builder) /// is expected to register its validation services into the DI container. /// public static IRCommonBuilder WithValidation(this IRCommonBuilder builder, Action actions) - where T : IValidationBuilder + where T : class, IValidationBuilder { - // Instantiate the validation builder via reflection; the constructor registers validation services into DI - var cqrsBuilder = (T)Activator.CreateInstance(typeof(T), new object[] { builder })!; + // Instantiate the validation builder via reflection; the constructor registers validation services into DI. + // Routed through GetOrAddBuilder so repeated WithValidation calls reuse the cached sub-builder. + var cqrsBuilder = builder.GetOrAddBuilder( + () => (T)Activator.CreateInstance(typeof(T), new object[] { builder })!); builder.Services.Configure(actions); return builder; } diff --git a/Src/RCommon.Json/JsonBuilderExtensions.cs b/Src/RCommon.Json/JsonBuilderExtensions.cs index 53077341..062986f3 100644 --- a/Src/RCommon.Json/JsonBuilderExtensions.cs +++ b/Src/RCommon.Json/JsonBuilderExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using RCommon.Bootstrapping; using System; using System.Collections.Generic; using System.Linq; @@ -26,7 +27,7 @@ public static class JsonBuilderExtensions /// The RCommon builder instance. /// The for further chaining. public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder builder) - where T : IJsonBuilder + where T : class, IJsonBuilder { return WithJsonSerialization(builder, x => { }, x => { }, x => { }); } @@ -41,7 +42,7 @@ public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder buil /// The for further chaining. public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder builder, Action serializeOptions, Action deSerializeOptions) - where T : IJsonBuilder + where T : class, IJsonBuilder { return WithJsonSerialization(builder, serializeOptions, deSerializeOptions, x => { }); } @@ -54,7 +55,7 @@ public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder buil /// An action to configure . /// The for further chaining. public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder builder, Action serializeOptions) - where T : IJsonBuilder + where T : class, IJsonBuilder { return WithJsonSerialization(builder, serializeOptions, x => { }, x => { }); } @@ -68,7 +69,7 @@ public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder buil /// The for further chaining. public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder builder, Action deSerializeOptions) - where T : IJsonBuilder + where T : class, IJsonBuilder { return WithJsonSerialization(builder, x => { }, deSerializeOptions, x => { }); } @@ -81,7 +82,7 @@ public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder buil /// An action to further configure the builder instance. /// The for further chaining. public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder builder, Action actions) - where T : IJsonBuilder + where T : class, IJsonBuilder { return WithJsonSerialization(builder, x => { }, x => { }, actions); @@ -98,21 +99,40 @@ public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder buil /// An action to further configure the builder instance. /// The for further chaining. /// + /// Singleton-style: re-invoking with the same is idempotent (the sub-builder + /// is cached via so the + /// constructor runs once); the delegate still runs each call. + /// Re-invoking with a different concrete after one is already configured + /// throws . /// This method uses to instantiate the builder, /// passing the as the constructor argument. The builder's constructor /// is expected to register its serialization services into the DI container. /// public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder builder, Action serializeOptions, Action deSerializeOptions, Action actions) - where T : IJsonBuilder + where T : class, IJsonBuilder { Guard.IsNotNull(serializeOptions, nameof(serializeOptions)); Guard.IsNotNull(deSerializeOptions, nameof(deSerializeOptions)); Guard.IsNotNull(actions, nameof(actions)); + + var existing = RCommonBuilderInternals.FindCachedImplementationOf(builder); + if (existing is not null && existing != typeof(T)) + { + throw new RCommonBuilderException( + $"IJsonBuilder already configured as '{existing.FullName}'; " + + $"cannot reconfigure as '{typeof(T).FullName}'. " + + "To configure multiple modules consistently, ensure all modules agree on the same JSON serialization implementation."); + } + builder.Services.Configure(serializeOptions); + // NOTE: deSerializeOptions is intentionally not wired up here. The existing implementation + // also does not call Configure. Preserving that pre-existing behavior + // keeps this change scope-limited to the singleton-style migration; the missing wiring is + // tracked separately as a follow-up. - // Instantiate the builder via reflection; the constructor registers serializer services into DI - var jsonConfig = (T)Activator.CreateInstance(typeof(T), new object[] { builder })!; + var jsonConfig = builder.GetOrAddBuilder( + () => (T)Activator.CreateInstance(typeof(T), new object[] { builder })!); actions(jsonConfig); return builder; } diff --git a/Src/RCommon.MassTransit.StateMachines/MassTransitStateMachineBuilderExtensions.cs b/Src/RCommon.MassTransit.StateMachines/MassTransitStateMachineBuilderExtensions.cs index 06875360..3f190200 100644 --- a/Src/RCommon.MassTransit.StateMachines/MassTransitStateMachineBuilderExtensions.cs +++ b/Src/RCommon.MassTransit.StateMachines/MassTransitStateMachineBuilderExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using RCommon.MassTransit.StateMachines; using RCommon.StateMachines; @@ -18,7 +19,7 @@ public static class MassTransitStateMachineBuilderExtensions /// The for further chaining. public static IRCommonBuilder WithMassTransitStateMachine(this IRCommonBuilder builder) { - builder.Services.AddTransient(typeof(IStateMachineConfigurator<,>), typeof(MassTransitStateMachineConfigurator<,>)); + builder.Services.TryAddTransient(typeof(IStateMachineConfigurator<,>), typeof(MassTransitStateMachineConfigurator<,>)); return builder; } } diff --git a/Src/RCommon.MassTransit/MassTransitEventHandlingBuilderExtensions.cs b/Src/RCommon.MassTransit/MassTransitEventHandlingBuilderExtensions.cs index bc005fae..a6bd2b0b 100644 --- a/Src/RCommon.MassTransit/MassTransitEventHandlingBuilderExtensions.cs +++ b/Src/RCommon.MassTransit/MassTransitEventHandlingBuilderExtensions.cs @@ -33,7 +33,8 @@ public static class MassTransitEventHandlingBuilderExtensions /// Optional configuration action for . /// The for further chaining. /// Thrown if MassTransit has already been registered in this container. - private static IServiceCollection AddMassTransit(this IRCommonBuilder builder, Action? configure = null) + private static IServiceCollection AddMassTransit(this IRCommonBuilder builder, Action? configure = null) + where T : class, IMassTransitEventHandlingBuilder { if (builder.Services.Any(d => d.ServiceType == typeof(IBus))) { @@ -43,10 +44,17 @@ private static IServiceCollection AddMassTransit(this IRCommonBuilder builder, A AddHostedService(builder.Services); AddInstrumentation(builder.Services); - - var configurator = new MassTransitEventHandlingBuilder(builder); + + // Routed through GetOrAddBuilder so repeated WithEventHandling calls reuse the cached sub-builder. + var configurator = builder.GetOrAddBuilder( + () => (T)Activator.CreateInstance(typeof(T), new object[] { builder })!); configure?.Invoke(configurator); - configurator.Complete(); + + // Complete() must run on the concrete MassTransit base type to finalize bus registration. + if (configurator is MassTransitEventHandlingBuilder mtBuilder) + { + mtBuilder.Complete(); + } return builder.Services; } @@ -84,7 +92,7 @@ private static void AddInstrumentation(IServiceCollection collection) /// The RCommon builder. /// The for further chaining. public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder) - where T : IMassTransitEventHandlingBuilder + where T : class, IMassTransitEventHandlingBuilder { return WithEventHandling(builder, x => { }); } @@ -99,13 +107,13 @@ public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder) /// Configuration delegate for MassTransit event handling. /// The for further chaining. public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder, Action actions) - where T : IMassTransitEventHandlingBuilder + where T : class, IMassTransitEventHandlingBuilder { - + // MassTransit Event Bus builder.Services.AddScoped(typeof(IMassTransitEventHandler<>), typeof(MassTransitEventHandler<>)); builder.Services.AddScoped(typeof(MassTransitEventHandler<>)); - builder.AddMassTransit(actions); + builder.AddMassTransit(actions); return builder; } diff --git a/Src/RCommon.Mediator/MediatorBuilderExtensions.cs b/Src/RCommon.Mediator/MediatorBuilderExtensions.cs index fe70defd..898982f7 100644 --- a/Src/RCommon.Mediator/MediatorBuilderExtensions.cs +++ b/Src/RCommon.Mediator/MediatorBuilderExtensions.cs @@ -25,7 +25,7 @@ public static class MediatorBuilderExtensions /// The RCommon builder instance. /// The for chaining additional configuration calls. public static IRCommonBuilder WithMediator(this IRCommonBuilder builder) - where T : IMediatorBuilder + where T : class, IMediatorBuilder { return WithMediator(builder, x => { }); } @@ -45,13 +45,15 @@ public static IRCommonBuilder WithMediator(this IRCommonBuilder builder) /// the configuration delegate to allow library-specific setup. /// public static IRCommonBuilder WithMediator(this IRCommonBuilder builder, Action actions) - where T : IMediatorBuilder + where T : class, IMediatorBuilder { builder.Services.AddScoped(); - // Create the mediator-specific builder by convention (expects a constructor accepting IRCommonBuilder) - var mediatorConfig = (T)Activator.CreateInstance(typeof(T), new object[] { builder })!; + // Create the mediator-specific builder by convention (expects a constructor accepting IRCommonBuilder). + // Routed through GetOrAddBuilder so repeated WithMediator calls reuse the cached sub-builder. + var mediatorConfig = builder.GetOrAddBuilder( + () => (T)Activator.CreateInstance(typeof(T), new object[] { builder })!); actions(mediatorConfig); return builder; } diff --git a/Src/RCommon.Mediatr/MediatREventHandlingBuilderExtensions.cs b/Src/RCommon.Mediatr/MediatREventHandlingBuilderExtensions.cs index caf931c8..3a916e68 100644 --- a/Src/RCommon.Mediatr/MediatREventHandlingBuilderExtensions.cs +++ b/Src/RCommon.Mediatr/MediatREventHandlingBuilderExtensions.cs @@ -28,7 +28,7 @@ public static class MediatREventHandlingBuilderExtensions /// The RCommon builder. /// The for further chaining. public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder) - where T : IMediatREventHandlingBuilder + where T : class, IMediatREventHandlingBuilder { return WithEventHandling(builder, x => { }, x=> { }); } @@ -41,7 +41,7 @@ public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder) /// Configuration delegate for MediatR event handling. /// The for further chaining. public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder, Action actions) - where T : IMediatREventHandlingBuilder + where T : class, IMediatREventHandlingBuilder { // MediatR WithEventHandling(builder, actions, mediatrActions => @@ -63,15 +63,17 @@ public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder, /// The for further chaining. public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder, Action actions, Action mediatRActions) - where T : IMediatREventHandlingBuilder + where T : class, IMediatREventHandlingBuilder { builder.Services.AddScoped(); // MediatR builder.Services.AddMediatR(mediatRActions); - // This will wire up common event handling - var eventHandlingConfig = (T)Activator.CreateInstance(typeof(T), new object[] { builder })!; + // This will wire up common event handling. + // Routed through GetOrAddBuilder so repeated WithEventHandling calls reuse the cached sub-builder. + var eventHandlingConfig = builder.GetOrAddBuilder( + () => (T)Activator.CreateInstance(typeof(T), new object[] { builder })!); actions(eventHandlingConfig); return builder; diff --git a/Src/RCommon.MultiTenancy/MultiTenancyBuilderExtensions.cs b/Src/RCommon.MultiTenancy/MultiTenancyBuilderExtensions.cs index acf81568..c6d2405c 100644 --- a/Src/RCommon.MultiTenancy/MultiTenancyBuilderExtensions.cs +++ b/Src/RCommon.MultiTenancy/MultiTenancyBuilderExtensions.cs @@ -16,9 +16,12 @@ public static class MultiTenancyBuilderExtensions /// An action to configure the multitenancy provider. /// The for fluent chaining. public static IRCommonBuilder WithMultiTenancy(this IRCommonBuilder builder, Action actions) - where TBuilder : IMultiTenantBuilder + where TBuilder : class, IMultiTenantBuilder { - var multiTenantBuilder = (TBuilder)Activator.CreateInstance(typeof(TBuilder), new object[] { builder.Services })!; + // Routed through GetOrAddBuilder so repeated WithMultiTenancy calls for the same + // provider type reuse the cached sub-builder rather than constructing a fresh one each call. + var multiTenantBuilder = builder.GetOrAddBuilder( + () => (TBuilder)Activator.CreateInstance(typeof(TBuilder), new object[] { builder.Services })!); actions(multiTenantBuilder); return builder; } diff --git a/Src/RCommon.Persistence/DataStoreFactoryOptions.cs b/Src/RCommon.Persistence/DataStoreFactoryOptions.cs index 49701521..6605df82 100644 --- a/Src/RCommon.Persistence/DataStoreFactoryOptions.cs +++ b/Src/RCommon.Persistence/DataStoreFactoryOptions.cs @@ -25,22 +25,36 @@ public class DataStoreFactoryOptions /// The base data store type (e.g., a provider-specific DbContext base). /// The concrete data store type that implements . /// A unique name identifying the data store registration. + /// + /// Re-registering the exact same (name, base, concrete) triple is a no-op so modules can + /// safely declare the same data store independently. A conflicting registration (same + /// and base but a different concrete + /// ) throws. + /// /// - /// Thrown when a data store with the same and base type is already registered. + /// Thrown when a data store with the same and base type + /// is already registered with a different concrete type than + /// . /// public void Register(string name) where B : IDataStore where C : IDataStore { - // Prevent duplicate registrations with the same name and base type - if (!Values.Any(x => x.Name == name && x.BaseType == typeof(B))) + var existing = Values.FirstOrDefault(x => x.Name == name && x.BaseType == typeof(B)); + if (existing is null) { Values.Add(new DataStoreValue(name, typeof(B), typeof(C))); + return; } - else + + if (existing.ConcreteType == typeof(C)) { - throw new UnsupportedDataStoreException($"You cannot register a data store with the same name of {name} as an existing one with the same base type of {typeof(B).GetGenericTypeName()}"); + return; } + + throw new UnsupportedDataStoreException( + $"Data store '{name}' for base type '{typeof(B).GetGenericTypeName()}' is already registered with concrete type " + + $"'{existing.ConcreteType.GetGenericTypeName()}'; cannot reconfigure as '{typeof(C).GetGenericTypeName()}'."); } } } diff --git a/Src/RCommon.Persistence/PersistenceBuilderExtensions.cs b/Src/RCommon.Persistence/PersistenceBuilderExtensions.cs index 4eabb1a5..2490f5a1 100644 --- a/Src/RCommon.Persistence/PersistenceBuilderExtensions.cs +++ b/Src/RCommon.Persistence/PersistenceBuilderExtensions.cs @@ -28,7 +28,7 @@ public static class PersistenceBuilderExtensions /// The RCommon builder instance. /// The for fluent chaining. public static IRCommonBuilder WithPersistence(this IRCommonBuilder builder) - where TObjectAccess : IPersistenceBuilder + where TObjectAccess : class, IPersistenceBuilder { return WithPersistence(builder, x => { }); } @@ -41,13 +41,13 @@ public static IRCommonBuilder WithPersistence(this IRCommonBuilde /// An action to configure the persistence provider (e.g., register data stores). /// The for fluent chaining. public static IRCommonBuilder WithPersistence(this IRCommonBuilder builder, Action objectAccessActions) - where TObjectAccess : IPersistenceBuilder + where TObjectAccess : class, IPersistenceBuilder { // Register NullTenantIdAccessor as default; concrete multitenancy packages override this builder.Services.TryAddTransient(); - // Create the persistence builder via Activator since the concrete type is not known at compile time - var dataConfiguration = (TObjectAccess)Activator.CreateInstance(typeof(TObjectAccess), new object[] { builder.Services })!; + var dataConfiguration = builder.GetOrAddBuilder( + () => (TObjectAccess)Activator.CreateInstance(typeof(TObjectAccess), new object[] { builder.Services })!); objectAccessActions(dataConfiguration); builder = WithEventTracking(builder); return builder; @@ -61,9 +61,10 @@ public static IRCommonBuilder WithPersistence(this IRCommonBuilde /// An action to configure the unit of work (e.g., set isolation level, auto-complete). /// The for fluent chaining. public static IRCommonBuilder WithUnitOfWork(this IRCommonBuilder builder, Action unitOfWorkActions) - where TUnitOfWork : IUnitOfWorkBuilder + where TUnitOfWork : class, IUnitOfWorkBuilder { - var unitOfWorkConfiguration = (TUnitOfWork)Activator.CreateInstance(typeof(TUnitOfWork), new object[] { builder.Services })!; + var unitOfWorkConfiguration = builder.GetOrAddBuilder( + () => (TUnitOfWork)Activator.CreateInstance(typeof(TUnitOfWork), new object[] { builder.Services })!); unitOfWorkActions(unitOfWorkConfiguration); return builder; } @@ -76,8 +77,8 @@ public static IRCommonBuilder WithUnitOfWork(this IRCommonBuilder b /// Updated instance of RCommon Configuration private static IRCommonBuilder WithEventTracking(this IRCommonBuilder builder) { - builder.Services.AddScoped(); - builder.Services.AddScoped(); + builder.Services.TryAddScoped(); + builder.Services.TryAddScoped(); return builder; } @@ -92,8 +93,8 @@ private static IRCommonBuilder WithEventTracking(this IRCommonBuilder builder) /// [Obsolete("This is deprecated as peristence is decoupled from unit of work.")] public static IRCommonBuilder WithPersistence(this IRCommonBuilder builder) - where TObjectAccess: IPersistenceBuilder - where TUnitOfWork : IUnitOfWorkBuilder + where TObjectAccess : class, IPersistenceBuilder + where TUnitOfWork : class, IUnitOfWorkBuilder { return WithPersistence(builder, x => { }, x => { }); } @@ -110,8 +111,8 @@ public static IRCommonBuilder WithPersistence(this I [Obsolete("This is deprecated as peristence is decoupled from unit of work.")] public static IRCommonBuilder WithPersistence(this IRCommonBuilder builder, Action objectAccessActions) - where TObjectAccess : IPersistenceBuilder - where TUnitOfWork : IUnitOfWorkBuilder + where TObjectAccess : class, IPersistenceBuilder + where TUnitOfWork : class, IUnitOfWorkBuilder { return WithPersistence(builder, objectAccessActions, x => { }); } @@ -128,8 +129,8 @@ public static IRCommonBuilder WithPersistence(this I [Obsolete("This is deprecated as peristence is decoupled from unit of work.")] public static IRCommonBuilder WithPersistence(this IRCommonBuilder builder, Action uniOfWorkActions) - where TObjectAccess : IPersistenceBuilder - where TUnitOfWork : IUnitOfWorkBuilder + where TObjectAccess : class, IPersistenceBuilder + where TUnitOfWork : class, IUnitOfWorkBuilder { return WithPersistence(builder, x => { }, uniOfWorkActions); } @@ -145,14 +146,16 @@ public static IRCommonBuilder WithPersistence(this I /// /// [Obsolete("This is deprecated as peristence is decoupled from unit of work.")] - public static IRCommonBuilder WithPersistence(this IRCommonBuilder builder, + public static IRCommonBuilder WithPersistence(this IRCommonBuilder builder, Action objectAccessActions, Action unitOfWorkActions) - where TObjectAccess : IPersistenceBuilder - where TUnitOfWork : IUnitOfWorkBuilder + where TObjectAccess : class, IPersistenceBuilder + where TUnitOfWork : class, IUnitOfWorkBuilder { - var dataConfiguration = (TObjectAccess)Activator.CreateInstance(typeof(TObjectAccess), new object[] { builder.Services })!; + var dataConfiguration = builder.GetOrAddBuilder( + () => (TObjectAccess)Activator.CreateInstance(typeof(TObjectAccess), new object[] { builder.Services })!); objectAccessActions(dataConfiguration); - var unitOfWorkConfiguration = (TUnitOfWork)Activator.CreateInstance(typeof(TUnitOfWork), new object[] { builder.Services })!; + var unitOfWorkConfiguration = builder.GetOrAddBuilder( + () => (TUnitOfWork)Activator.CreateInstance(typeof(TUnitOfWork), new object[] { builder.Services })!); unitOfWorkActions(unitOfWorkConfiguration); builder = WithEventTracking(builder); return builder; diff --git a/Src/RCommon.Security/SecurityConfigurationExtensions.cs b/Src/RCommon.Security/SecurityConfigurationExtensions.cs index 3e502b16..6048e3fd 100644 --- a/Src/RCommon.Security/SecurityConfigurationExtensions.cs +++ b/Src/RCommon.Security/SecurityConfigurationExtensions.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using RCommon.Security.Users; using RCommon.Security.Claims; using RCommon.Security.Clients; @@ -24,10 +25,10 @@ public static class SecurityConfigurationExtensions /// The same instance for fluent chaining. public static IRCommonBuilder WithClaimsAndPrincipalAccessor(this IRCommonBuilder config) { - config.Services.AddTransient(); - config.Services.AddTransient(); - config.Services.AddTransient(); - config.Services.AddTransient(); + config.Services.TryAddTransient(); + config.Services.TryAddTransient(); + config.Services.TryAddTransient(); + config.Services.TryAddTransient(); return config; } } diff --git a/Src/RCommon.SendGrid/SendGridEmailingConfigurationExtensions.cs b/Src/RCommon.SendGrid/SendGridEmailingConfigurationExtensions.cs index 73431c90..ff9b3dbe 100644 --- a/Src/RCommon.SendGrid/SendGridEmailingConfigurationExtensions.cs +++ b/Src/RCommon.SendGrid/SendGridEmailingConfigurationExtensions.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using RCommon.Emailing; using RCommon.Emailing.SendGrid; using RCommon.Emailing.Smtp; @@ -22,10 +23,23 @@ public static class SendGridEmailingConfigurationExtensions /// The RCommon builder to configure. /// A delegate to configure . /// The same instance for fluent chaining. + /// + /// Thrown when has already been registered with a different + /// implementation type, since mixing email service implementations is unsupported. + /// public static IRCommonBuilder WithSendGridEmailServices(this IRCommonBuilder config, Action emailSettings) { + var existing = config.Services.FirstOrDefault(d => d.ServiceType == typeof(IEmailService)); + if (existing?.ImplementationType is { } existingImpl && existingImpl != typeof(SendGridEmailService)) + { + throw new RCommonBuilderException( + $"IEmailService already configured as '{existingImpl.FullName}'; cannot reconfigure as '{typeof(SendGridEmailService).FullName}'. " + + "To configure multiple modules consistently, ensure all modules agree on the same IEmailService implementation, " + + "or designate a single composition root that performs this registration."); + } + config.Services.Configure(emailSettings); - config.Services.AddTransient(); + config.Services.TryAddTransient(); return config; } } diff --git a/Src/RCommon.Stateless/StatelessBuilderExtensions.cs b/Src/RCommon.Stateless/StatelessBuilderExtensions.cs index 8ebb4035..a7ce47c3 100644 --- a/Src/RCommon.Stateless/StatelessBuilderExtensions.cs +++ b/Src/RCommon.Stateless/StatelessBuilderExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using RCommon.Stateless; using RCommon.StateMachines; @@ -18,7 +19,7 @@ public static class StatelessBuilderExtensions /// The for further chaining. public static IRCommonBuilder WithStatelessStateMachine(this IRCommonBuilder builder) { - builder.Services.AddTransient(typeof(IStateMachineConfigurator<,>), typeof(StatelessConfigurator<,>)); + builder.Services.TryAddTransient(typeof(IStateMachineConfigurator<,>), typeof(StatelessConfigurator<,>)); return builder; } } diff --git a/Src/RCommon.Web/WebConfigurationExtensions.cs b/Src/RCommon.Web/WebConfigurationExtensions.cs index 60065005..8119a11f 100644 --- a/Src/RCommon.Web/WebConfigurationExtensions.cs +++ b/Src/RCommon.Web/WebConfigurationExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using RCommon.Security.Claims; using RCommon.Security.Clients; using RCommon.Security.Users; @@ -27,10 +28,10 @@ public static class WebConfigurationExtensions public static IRCommonBuilder WithClaimsAndPrincipalAccessorForWeb(this IRCommonBuilder config) { config.Services.AddHttpContextAccessor(); - config.Services.AddTransient(); - config.Services.AddTransient(); - config.Services.AddTransient(); - config.Services.AddTransient(); + config.Services.TryAddTransient(); + config.Services.TryAddTransient(); + config.Services.TryAddTransient(); + config.Services.TryAddTransient(); return config; } } diff --git a/Tests/RCommon.Caching.Tests/CachingBuilderExtensionsTests.cs b/Tests/RCommon.Caching.Tests/CachingBuilderExtensionsTests.cs index cd031c76..f6f18b66 100644 --- a/Tests/RCommon.Caching.Tests/CachingBuilderExtensionsTests.cs +++ b/Tests/RCommon.Caching.Tests/CachingBuilderExtensionsTests.cs @@ -290,7 +290,7 @@ public void FluentChaining_WithOtherBuilderMethods_WorksCorrectly() #region Builder Instance Creation Tests [Fact] - public void WithMemoryCaching_CreatesNewBuilderInstanceEachTime() + public void WithMemoryCaching_ReusesCachedBuilderForRepeatedCalls() { // Arrange var services = new ServiceCollection(); @@ -303,13 +303,15 @@ public void WithMemoryCaching_CreatesNewBuilderInstanceEachTime() rcommonBuilder.WithMemoryCaching(builder => secondBuilder = builder); // Assert + // The action delegate still runs each call, but the sub-builder is cached + // via IRCommonBuilder.GetOrAddBuilder so both invocations see the same instance. firstBuilder.Should().NotBeNull(); secondBuilder.Should().NotBeNull(); - firstBuilder.Should().NotBeSameAs(secondBuilder); + firstBuilder.Should().BeSameAs(secondBuilder); } [Fact] - public void WithDistributedCaching_CreatesNewBuilderInstanceEachTime() + public void WithDistributedCaching_ReusesCachedBuilderForRepeatedCalls() { // Arrange var services = new ServiceCollection(); @@ -322,9 +324,11 @@ public void WithDistributedCaching_CreatesNewBuilderInstanceEachTime() rcommonBuilder.WithDistributedCaching(builder => secondBuilder = builder); // Assert + // The action delegate still runs each call, but the sub-builder is cached + // via IRCommonBuilder.GetOrAddBuilder so both invocations see the same instance. firstBuilder.Should().NotBeNull(); secondBuilder.Should().NotBeNull(); - firstBuilder.Should().NotBeSameAs(secondBuilder); + firstBuilder.Should().BeSameAs(secondBuilder); } #endregion diff --git a/Tests/RCommon.Core.Tests/Bootstrapping/AddRCommonIdempotencyTests.cs b/Tests/RCommon.Core.Tests/Bootstrapping/AddRCommonIdempotencyTests.cs new file mode 100644 index 00000000..9d21ecb3 --- /dev/null +++ b/Tests/RCommon.Core.Tests/Bootstrapping/AddRCommonIdempotencyTests.cs @@ -0,0 +1,85 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using RCommon.EventHandling; +using RCommon.EventHandling.Producers; +using Xunit; + +namespace RCommon.Core.Tests.Bootstrapping; + +public class AddRCommonIdempotencyTests +{ + [Fact] + public void AddRCommon_CalledTwice_ReturnsSameBuilderInstance() + { + var services = new ServiceCollection(); + + var first = services.AddRCommon(); + var second = services.AddRCommon(); + + second.Should().BeSameAs(first); + } + + [Fact] + public void AddRCommon_CalledTwice_RegistersEventBusOnlyOnce() + { + var services = new ServiceCollection(); + + services.AddRCommon(); + services.AddRCommon(); + + services.Count(d => d.ServiceType == typeof(IEventBus)).Should().Be(1); + } + + [Fact] + public void AddRCommon_CalledTwice_RegistersEventSubscriptionManagerOnlyOnce() + { + var services = new ServiceCollection(); + + services.AddRCommon(); + services.AddRCommon(); + + services.Count(d => d.ServiceType == typeof(EventSubscriptionManager)).Should().Be(1); + } + + [Fact] + public void AddRCommon_CalledTwice_RegistersEventRouterOnlyOnce() + { + var services = new ServiceCollection(); + + services.AddRCommon(); + services.AddRCommon(); + + services.Count(d => d.ServiceType == typeof(IEventRouter)).Should().Be(1); + } + + [Fact] + public void AddRCommon_CalledTwice_HasIdenticalDescriptorCountToCalledOnce() + { + var servicesA = new ServiceCollection(); + var servicesB = new ServiceCollection(); + + servicesA.AddRCommon(); + servicesB.AddRCommon(); + servicesB.AddRCommon(); + + servicesB.Count.Should().Be(servicesA.Count); + } + + [Fact] + public void IsRCommonInitialized_BeforeAddRCommon_ReturnsFalse() + { + var services = new ServiceCollection(); + + services.IsRCommonInitialized().Should().BeFalse(); + } + + [Fact] + public void IsRCommonInitialized_AfterAddRCommon_ReturnsTrue() + { + var services = new ServiceCollection(); + + services.AddRCommon(); + + services.IsRCommonInitialized().Should().BeTrue(); + } +} diff --git a/Tests/RCommon.Core.Tests/Bootstrapping/EventProducerDedupTests.cs b/Tests/RCommon.Core.Tests/Bootstrapping/EventProducerDedupTests.cs new file mode 100644 index 00000000..1e67e971 --- /dev/null +++ b/Tests/RCommon.Core.Tests/Bootstrapping/EventProducerDedupTests.cs @@ -0,0 +1,64 @@ +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using RCommon.EventHandling; +using RCommon.EventHandling.Producers; +using RCommon.Models.Events; +using Xunit; + +namespace RCommon.Core.Tests.Bootstrapping; + +public class EventProducerDedupTests +{ + [Fact] + public void AddProducer_SameTypeCalledTwice_RegistersOnce() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + builder.WithEventHandling(eh => + { + eh.AddProducer(); + eh.AddProducer(); + }); + + var producerDescriptors = services + .Where(d => d.ServiceType == typeof(IEventProducer) && d.ImplementationType == typeof(TestProducer)) + .ToList(); + producerDescriptors.Should().HaveCount(1); + } + + [Fact] + public void AddProducer_DifferentTypes_RegistersBoth() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + builder.WithEventHandling(eh => + { + eh.AddProducer(); + eh.AddProducer(); + }); + + services.Count(d => d.ServiceType == typeof(IEventProducer)).Should().Be(2); + } + + public class TestEventHandlingBuilder : IEventHandlingBuilder + { + public TestEventHandlingBuilder(IRCommonBuilder builder) { Services = builder.Services; } + public IServiceCollection Services { get; } + } + + public class TestProducer : IEventProducer + { + public Task ProduceEventAsync(TEvent @event, CancellationToken cancellationToken = default) + where TEvent : ISerializableEvent => Task.CompletedTask; + } + + public class OtherTestProducer : IEventProducer + { + public Task ProduceEventAsync(TEvent @event, CancellationToken cancellationToken = default) + where TEvent : ISerializableEvent => Task.CompletedTask; + } +} diff --git a/Tests/RCommon.Core.Tests/Bootstrapping/SingletonVerbConflictTests.cs b/Tests/RCommon.Core.Tests/Bootstrapping/SingletonVerbConflictTests.cs new file mode 100644 index 00000000..127faf32 --- /dev/null +++ b/Tests/RCommon.Core.Tests/Bootstrapping/SingletonVerbConflictTests.cs @@ -0,0 +1,73 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace RCommon.Core.Tests.Bootstrapping; + +public class SingletonVerbConflictTests +{ + [Fact] + public void WithSimpleGuidGenerator_CalledTwice_IsIdempotent() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + builder.WithSimpleGuidGenerator(); + Action secondCall = () => builder.WithSimpleGuidGenerator(); + + secondCall.Should().NotThrow(); + services.Count(d => d.ServiceType == typeof(IGuidGenerator)).Should().Be(1); + } + + [Fact] + public void WithSequentialGuidGenerator_CalledTwice_IsIdempotent() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + builder.WithSequentialGuidGenerator(o => { }); + Action secondCall = () => builder.WithSequentialGuidGenerator(o => { }); + + secondCall.Should().NotThrow(); + services.Count(d => d.ServiceType == typeof(IGuidGenerator)).Should().Be(1); + } + + [Fact] + public void WithSimpleGuidGenerator_AfterSequentialGuidGenerator_ThrowsRCommonBuilderException() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + builder.WithSequentialGuidGenerator(o => { }); + + Action act = () => builder.WithSimpleGuidGenerator(); + + act.Should().Throw() + .WithMessage("*SequentialGuidGenerator*SimpleGuidGenerator*"); + } + + [Fact] + public void WithSequentialGuidGenerator_AfterSimpleGuidGenerator_ThrowsRCommonBuilderException() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + builder.WithSimpleGuidGenerator(); + + Action act = () => builder.WithSequentialGuidGenerator(o => { }); + + act.Should().Throw() + .WithMessage("*SimpleGuidGenerator*SequentialGuidGenerator*"); + } + + [Fact] + public void WithDateTimeSystem_CalledTwice_IsIdempotent() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + builder.WithDateTimeSystem(o => { }); + Action secondCall = () => builder.WithDateTimeSystem(o => { }); + + secondCall.Should().NotThrow(); + services.Count(d => d.ServiceType == typeof(ISystemTime)).Should().Be(1); + } +} diff --git a/Tests/RCommon.Core.Tests/Bootstrapping/SoftDuplicateDiagnosticsTests.cs b/Tests/RCommon.Core.Tests/Bootstrapping/SoftDuplicateDiagnosticsTests.cs new file mode 100644 index 00000000..183f9b3b --- /dev/null +++ b/Tests/RCommon.Core.Tests/Bootstrapping/SoftDuplicateDiagnosticsTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace RCommon.Core.Tests.Bootstrapping; + +public class SoftDuplicateDiagnosticsTests +{ + [Fact] + public async Task HostedService_WithSoftDuplicates_EmitsSingleWarning() + { + var capturedWarnings = new List(); + var services = new ServiceCollection(); + services.AddSingleton(new TestLoggerFactory(capturedWarnings)); + services.AddLogging(); + + services.AddRCommon(); + // Inject a duplicate registration to trigger soft-duplicate detection + services.AddTransient(); + services.AddTransient(); + + var provider = services.BuildServiceProvider(); + foreach (var hs in provider.GetServices()) + { + await hs.StartAsync(CancellationToken.None); + } + + capturedWarnings.Should().HaveCount(1); + capturedWarnings[0].Should().Contain("FakeServiceImpl"); + } + + [Fact] + public async Task HostedService_NoSoftDuplicates_EmitsNoWarning() + { + var capturedWarnings = new List(); + var services = new ServiceCollection(); + services.AddSingleton(new TestLoggerFactory(capturedWarnings)); + services.AddLogging(); + + services.AddRCommon(); + + var provider = services.BuildServiceProvider(); + foreach (var hs in provider.GetServices()) + { + await hs.StartAsync(CancellationToken.None); + } + + capturedWarnings.Should().BeEmpty(); + } + + [Fact] + public async Task HostedService_CalledTwice_OnlyRunsScannerOnce() + { + var capturedWarnings = new List(); + var services = new ServiceCollection(); + services.AddSingleton(new TestLoggerFactory(capturedWarnings)); + services.AddLogging(); + + services.AddRCommon(); + services.AddTransient(); + services.AddTransient(); + + var provider = services.BuildServiceProvider(); + var hostedServices = provider.GetServices().ToList(); + foreach (var hs in hostedServices) + { + await hs.StartAsync(CancellationToken.None); + await hs.StartAsync(CancellationToken.None); + } + + capturedWarnings.Should().HaveCount(1); + } + + public interface IFakeService { } + public class FakeServiceImpl : IFakeService { } + + private sealed class TestLoggerFactory : ILoggerFactory + { + private readonly List _warnings; + public TestLoggerFactory(List warnings) { _warnings = warnings; } + public void AddProvider(ILoggerProvider provider) { } + public ILogger CreateLogger(string categoryName) => new TestLogger(_warnings); + public void Dispose() { } + } + + private sealed class TestLogger : ILogger + { + private readonly List _warnings; + public TestLogger(List warnings) { _warnings = warnings; } + public IDisposable? BeginScope(TState state) where TState : notnull => null; + public bool IsEnabled(LogLevel logLevel) => logLevel == LogLevel.Warning; + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (logLevel == LogLevel.Warning) + { + _warnings.Add(formatter(state, exception)); + } + } + } +} diff --git a/Tests/RCommon.Core.Tests/Bootstrapping/SubBuilderCacheTests.cs b/Tests/RCommon.Core.Tests/Bootstrapping/SubBuilderCacheTests.cs new file mode 100644 index 00000000..8cd635c2 --- /dev/null +++ b/Tests/RCommon.Core.Tests/Bootstrapping/SubBuilderCacheTests.cs @@ -0,0 +1,71 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace RCommon.Core.Tests.Bootstrapping; + +public class SubBuilderCacheTests +{ + [Fact] + public void GetOrAddBuilder_FirstCall_InvokesFactoryOnce() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + var factoryCount = 0; + + builder.GetOrAddBuilder(() => + { + factoryCount++; + return new TestSubBuilder(services); + }); + + factoryCount.Should().Be(1); + } + + [Fact] + public void GetOrAddBuilder_SecondCallForSameType_DoesNotInvokeFactory() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + var factoryCount = 0; + + builder.GetOrAddBuilder(() => { factoryCount++; return new TestSubBuilder(services); }); + builder.GetOrAddBuilder(() => { factoryCount++; return new TestSubBuilder(services); }); + + factoryCount.Should().Be(1); + } + + [Fact] + public void GetOrAddBuilder_SecondCallForSameType_ReturnsCachedInstance() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + var first = builder.GetOrAddBuilder(() => new TestSubBuilder(services)); + var second = builder.GetOrAddBuilder(() => new TestSubBuilder(services)); + + second.Should().BeSameAs(first); + } + + [Fact] + public void GetOrAddBuilder_DifferentTypes_ReturnsDistinctInstances() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + var subA = builder.GetOrAddBuilder(() => new TestSubBuilder(services)); + var subB = builder.GetOrAddBuilder(() => new OtherTestSubBuilder(services)); + + ((object)subA).Should().NotBeSameAs(subB); + } + + private sealed class TestSubBuilder + { + public TestSubBuilder(IServiceCollection services) { } + } + + private sealed class OtherTestSubBuilder + { + public OtherTestSubBuilder(IServiceCollection services) { } + } +} diff --git a/Tests/RCommon.Core.Tests/RCommonBuilderTests.cs b/Tests/RCommon.Core.Tests/RCommonBuilderTests.cs index 32bd8470..54f4bb8b 100644 --- a/Tests/RCommon.Core.Tests/RCommonBuilderTests.cs +++ b/Tests/RCommon.Core.Tests/RCommonBuilderTests.cs @@ -131,21 +131,6 @@ public void WithSequentialGuidGenerator_ConfiguresOptions() options!.Value.DefaultSequentialGuidType.Should().Be(SequentialGuidType.SequentialAsString); } - [Fact] - public void WithSequentialGuidGenerator_CalledTwice_ThrowsRCommonBuilderException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new RCommonBuilder(services); - builder.WithSequentialGuidGenerator(options => { }); - - // Act - var act = () => builder.WithSequentialGuidGenerator(options => { }); - - // Assert - act.Should().Throw(); - } - [Fact] public void WithSequentialGuidGenerator_ReturnsBuilder_ForFluentChaining() { @@ -182,21 +167,6 @@ public void WithSimpleGuidGenerator_RegistersSimpleGuidGenerator() guidGenerator.Should().BeOfType(); } - [Fact] - public void WithSimpleGuidGenerator_CalledTwice_ThrowsRCommonBuilderException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new RCommonBuilder(services); - builder.WithSimpleGuidGenerator(); - - // Act - var act = () => builder.WithSimpleGuidGenerator(); - - // Assert - act.Should().Throw(); - } - [Fact] public void WithSimpleGuidGenerator_AfterSequentialGuidGenerator_ThrowsRCommonBuilderException() { @@ -270,21 +240,6 @@ public void WithDateTimeSystem_ConfiguresOptions() options!.Value.Kind.Should().Be(DateTimeKind.Utc); } - [Fact] - public void WithDateTimeSystem_CalledTwice_ThrowsRCommonBuilderException() - { - // Arrange - var services = new ServiceCollection(); - var builder = new RCommonBuilder(services); - builder.WithDateTimeSystem(options => { }); - - // Act - var act = () => builder.WithDateTimeSystem(options => { }); - - // Assert - act.Should().Throw(); - } - [Fact] public void WithDateTimeSystem_ReturnsBuilder_ForFluentChaining() { diff --git a/Tests/RCommon.EfCore.Tests/Bootstrapping/MultiModuleEFCoreTests.cs b/Tests/RCommon.EfCore.Tests/Bootstrapping/MultiModuleEFCoreTests.cs new file mode 100644 index 00000000..0c01d655 --- /dev/null +++ b/Tests/RCommon.EfCore.Tests/Bootstrapping/MultiModuleEFCoreTests.cs @@ -0,0 +1,152 @@ +using System; +using System.Linq; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using RCommon.Persistence; +using RCommon.Persistence.Crud; +using RCommon.Persistence.EFCore; +using Xunit; + +namespace RCommon.EfCore.Tests.Bootstrapping; + +/// +/// End-to-end multi-module integration tests for +/// configured via AddRCommon().WithPersistence<EFCorePerisistenceBuilder>(...). +/// +/// Simulates two modules each independently calling AddRCommon().WithPersistence(...) +/// on the same and verifies the sub-builder cache, the +/// idempotent data-store registration semantics, and the conflict detection all behave +/// as documented. +/// +public class MultiModuleEFCoreTests +{ + [Fact] + public void TwoModules_DistinctDbContextsDistinctNames_BothResolvable() + { + // Arrange: two modules register their own DbContexts under distinct names + var services = new ServiceCollection(); + services.AddLogging(); + + services.AddRCommon().WithPersistence(ef => + ef.AddDbContext("DbA", o => o.UseInMemoryDatabase("DbA-" + Guid.NewGuid()))); + + services.AddRCommon().WithPersistence(ef => + ef.AddDbContext("DbB", o => o.UseInMemoryDatabase("DbB-" + Guid.NewGuid()))); + + // Act + using var provider = services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + var factory = scope.ServiceProvider.GetRequiredService(); + + // Use the single-generic Resolve(name) overload: it correctly filters by both + // name and base type. (The two-generic Resolve overload has a known issue + // where it peeks the first value in the bag without filtering -- out of scope here.) + var dbA = factory.Resolve("DbA"); + var dbB = factory.Resolve("DbB"); + + // Assert + dbA.Should().NotBeNull(); + dbA.Should().BeOfType(); + dbB.Should().NotBeNull(); + dbB.Should().BeOfType(); + } + + [Fact] + public void TwoModules_SameNameSameContext_IsIdempotent() + { + // Arrange: two modules each register the same DbContext under the same name + var services = new ServiceCollection(); + services.AddLogging(); + + services.AddRCommon().WithPersistence(ef => + ef.AddDbContext("DbA", o => o.UseInMemoryDatabase("DbA-Idem"))); + services.AddRCommon().WithPersistence(ef => + ef.AddDbContext("DbA", o => o.UseInMemoryDatabase("DbA-Idem"))); + + // Act: materialize the options to force the Register() validation to run + using var provider = services.BuildServiceProvider(); + Action materialize = () => + { + var opts = provider.GetRequiredService>().Value; + // Touch the values bag so the configure delegates have fully executed + _ = opts.Values.Count; + }; + + // Assert + materialize.Should().NotThrow(); + } + + [Fact] + public void TwoModules_SameNameDifferentContext_Throws() + { + // Arrange: two modules register different DbContext types under the same name + var services = new ServiceCollection(); + services.AddLogging(); + + services.AddRCommon().WithPersistence(ef => + ef.AddDbContext("DbA", o => o.UseInMemoryDatabase("DbA-Conflict-A"))); + services.AddRCommon().WithPersistence(ef => + ef.AddDbContext("DbA", o => o.UseInMemoryDatabase("DbA-Conflict-B"))); + + // Act: the conflict is detected when the options are materialized (Configure delegates + // run lazily inside IOptions.Value), so resolve IOptions to trigger the validation. + using var provider = services.BuildServiceProvider(); + Action materialize = () => + { + _ = provider.GetRequiredService>().Value; + }; + + // Assert: the Configure delegate registered by AddDbContext propagates + // UnsupportedDataStoreException directly out of IOptions.Value materialization. + materialize.Should().Throw() + .WithMessage("*DbA*TestDbContextA*TestDbContextB*"); + } + + [Fact] + public void TwoModules_RepositoryDescriptors_NotDuplicated() + { + // Arrange: two modules each call WithPersistence; the sub-builder cache should ensure + // the EFCorePerisistenceBuilder constructor runs only once, so the repository registrations + // it performs should each appear exactly once in the service collection. + var services = new ServiceCollection(); + services.AddLogging(); + + services.AddRCommon().WithPersistence(ef => + ef.AddDbContext("DbA", o => o.UseInMemoryDatabase("DbA-Dup1"))); + services.AddRCommon().WithPersistence(ef => + ef.AddDbContext("DbB", o => o.UseInMemoryDatabase("DbB-Dup1"))); + + // Assert: each generic repository interface registered by the sub-builder ctor should + // appear exactly once. + int CountOpenGeneric(Type openGenericServiceType) => + services.Count(d => + d.ServiceType.IsGenericTypeDefinition + ? d.ServiceType == openGenericServiceType + : d.ServiceType.IsGenericType && d.ServiceType.GetGenericTypeDefinition() == openGenericServiceType); + + CountOpenGeneric(typeof(IReadOnlyRepository<>)).Should().Be(1); + CountOpenGeneric(typeof(IWriteOnlyRepository<>)).Should().Be(1); + CountOpenGeneric(typeof(ILinqRepository<>)).Should().Be(1); + CountOpenGeneric(typeof(IGraphRepository<>)).Should().Be(1); + } + + /// + /// Test DbContext used by module A. Must inherit directly from + /// so that 's BaseType == typeof(RCommonDbContext) check + /// passes during registration. + /// + public class TestDbContextA : RCommonDbContext + { + public TestDbContextA(DbContextOptions options) : base(options) { } + } + + /// + /// Test DbContext used by module B. + /// + public class TestDbContextB : RCommonDbContext + { + public TestDbContextB(DbContextOptions options) : base(options) { } + } +} diff --git a/Tests/RCommon.Emailing.Tests/Bootstrapping/EmailServiceCrossImplConflictTests.cs b/Tests/RCommon.Emailing.Tests/Bootstrapping/EmailServiceCrossImplConflictTests.cs new file mode 100644 index 00000000..31b884d7 --- /dev/null +++ b/Tests/RCommon.Emailing.Tests/Bootstrapping/EmailServiceCrossImplConflictTests.cs @@ -0,0 +1,37 @@ +using System; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using RCommon.Emailing.SendGrid; +using RCommon.Emailing.Smtp; +using Xunit; + +namespace RCommon.Emailing.Tests.Bootstrapping; + +public class EmailServiceCrossImplConflictTests +{ + [Fact] + public void WithSmtpThenWithSendGrid_Throws() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + builder.WithSmtpEmailServices(opts => { }); + + Action act = () => builder.WithSendGridEmailServices(opts => { }); + + act.Should().Throw() + .WithMessage("*SmtpEmailService*SendGridEmailService*"); + } + + [Fact] + public void WithSendGridThenWithSmtp_Throws() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + builder.WithSendGridEmailServices(opts => { }); + + Action act = () => builder.WithSmtpEmailServices(opts => { }); + + act.Should().Throw() + .WithMessage("*SendGridEmailService*SmtpEmailService*"); + } +} diff --git a/Tests/RCommon.Emailing.Tests/Bootstrapping/SmtpEmailServicesSingletonTests.cs b/Tests/RCommon.Emailing.Tests/Bootstrapping/SmtpEmailServicesSingletonTests.cs new file mode 100644 index 00000000..ef9a380a --- /dev/null +++ b/Tests/RCommon.Emailing.Tests/Bootstrapping/SmtpEmailServicesSingletonTests.cs @@ -0,0 +1,23 @@ +using System.Linq; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using RCommon.Emailing; +using RCommon.Emailing.Smtp; +using Xunit; + +namespace RCommon.Emailing.Tests.Bootstrapping; + +public class SmtpEmailServicesSingletonTests +{ + [Fact] + public void WithSmtpEmailServices_CalledTwice_RegistersIEmailServiceOnce() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + builder.WithSmtpEmailServices(opts => { }); + builder.WithSmtpEmailServices(opts => { }); + + services.Count(d => d.ServiceType == typeof(IEmailService)).Should().Be(1); + } +} diff --git a/Tests/RCommon.Emailing.Tests/RCommon.Emailing.Tests.csproj b/Tests/RCommon.Emailing.Tests/RCommon.Emailing.Tests.csproj index b2591db9..59c51ff4 100644 --- a/Tests/RCommon.Emailing.Tests/RCommon.Emailing.Tests.csproj +++ b/Tests/RCommon.Emailing.Tests/RCommon.Emailing.Tests.csproj @@ -8,6 +8,7 @@ + diff --git a/Tests/RCommon.JsonNet.Tests/Bootstrapping/JsonSerializationSingletonTests.cs b/Tests/RCommon.JsonNet.Tests/Bootstrapping/JsonSerializationSingletonTests.cs new file mode 100644 index 00000000..239161ca --- /dev/null +++ b/Tests/RCommon.JsonNet.Tests/Bootstrapping/JsonSerializationSingletonTests.cs @@ -0,0 +1,51 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using RCommon.Json; +using RCommon.JsonNet; +using RCommon.SystemTextJson; +using Xunit; + +namespace RCommon.JsonNet.Tests.Bootstrapping; + +public class JsonSerializationSingletonTests +{ + [Fact] + public void WithJsonSerialization_SameType_IsIdempotent() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + builder.WithJsonSerialization(); + Action secondCall = () => builder.WithJsonSerialization(); + + secondCall.Should().NotThrow(); + } + + [Fact] + public void WithJsonSerialization_SameType_BothActionsApplied() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + var firstActionRan = false; + var secondActionRan = false; + + builder.WithJsonSerialization((Action)(b => firstActionRan = true)); + builder.WithJsonSerialization((Action)(b => secondActionRan = true)); + + firstActionRan.Should().BeTrue(); + secondActionRan.Should().BeTrue(); + } + + [Fact] + public void WithJsonSerialization_DifferentType_Throws() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + builder.WithJsonSerialization(); + + Action act = () => builder.WithJsonSerialization(); + + act.Should().Throw() + .WithMessage("*JsonNetBuilder*TextJsonBuilder*"); + } +} diff --git a/Tests/RCommon.JsonNet.Tests/RCommon.JsonNet.Tests.csproj b/Tests/RCommon.JsonNet.Tests/RCommon.JsonNet.Tests.csproj index abfc391a..2f6501c4 100644 --- a/Tests/RCommon.JsonNet.Tests/RCommon.JsonNet.Tests.csproj +++ b/Tests/RCommon.JsonNet.Tests/RCommon.JsonNet.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/Tests/RCommon.MassTransit.StateMachines.Tests/MassTransitStateMachineDITests.cs b/Tests/RCommon.MassTransit.StateMachines.Tests/MassTransitStateMachineDITests.cs index c69fa4af..ce15ba87 100644 --- a/Tests/RCommon.MassTransit.StateMachines.Tests/MassTransitStateMachineDITests.cs +++ b/Tests/RCommon.MassTransit.StateMachines.Tests/MassTransitStateMachineDITests.cs @@ -19,6 +19,9 @@ private class TestRCommonBuilder : IRCommonBuilder public IRCommonBuilder WithCommonFactory() where TService : class where TImplementation : class, TService => this; + public TSubBuilder GetOrAddBuilder(Func factory) + where TSubBuilder : class => factory(); + public string GetBootstrapDiagnostics() => string.Empty; } [Fact] diff --git a/Tests/RCommon.Mediator.Tests/MediatorBuilderExtensionsTests.cs b/Tests/RCommon.Mediator.Tests/MediatorBuilderExtensionsTests.cs index 4efed5f0..7cc2b6c5 100644 --- a/Tests/RCommon.Mediator.Tests/MediatorBuilderExtensionsTests.cs +++ b/Tests/RCommon.Mediator.Tests/MediatorBuilderExtensionsTests.cs @@ -240,7 +240,7 @@ public void WithMediator_MultipleCallsOverwriteRegistration() #region MediatorBuilder Creation Tests [Fact] - public void WithMediator_CreatesNewMediatorBuilderForEachCall() + public void WithMediator_ReusesCachedBuilderForRepeatedCalls() { // Arrange var services = new ServiceCollection(); @@ -252,8 +252,10 @@ public void WithMediator_CreatesNewMediatorBuilderForEachCall() builder.WithMediator(config => instances.Add(config)); // Assert + // The action delegate still runs each call, but the sub-builder is cached + // via IRCommonBuilder.GetOrAddBuilder so both invocations see the same instance. instances.Should().HaveCount(2); - instances[0].Should().NotBeSameAs(instances[1]); + instances[0].Should().BeSameAs(instances[1]); } #endregion @@ -284,6 +286,10 @@ public TestMediatorBuilder(IRCommonBuilder builder) public class TestRCommonBuilder : IRCommonBuilder { + // Mirrors the production RCommonBuilder._subBuilderCache so tests exercise the real caching contract: + // GetOrAddBuilder must return the same instance on repeated calls for the same T. + private readonly Dictionary _subBuilderCache = new(); + public IServiceCollection Services { get; } public TestRCommonBuilder(IServiceCollection services) @@ -317,6 +323,21 @@ public IRCommonBuilder WithCommonFactory() { return this; } + + public TSubBuilder GetOrAddBuilder(Func factory) + where TSubBuilder : class + { + if (_subBuilderCache.TryGetValue(typeof(TSubBuilder), out var existing)) + { + return (TSubBuilder)existing; + } + + var created = factory(); + _subBuilderCache[typeof(TSubBuilder)] = created; + return created; + } + + public string GetBootstrapDiagnostics() => string.Empty; } #endregion diff --git a/Tests/RCommon.Mediatr.Tests/Bootstrapping/MultiModuleMediatRTests.cs b/Tests/RCommon.Mediatr.Tests/Bootstrapping/MultiModuleMediatRTests.cs new file mode 100644 index 00000000..39e17940 --- /dev/null +++ b/Tests/RCommon.Mediatr.Tests/Bootstrapping/MultiModuleMediatRTests.cs @@ -0,0 +1,54 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using RCommon.EventHandling.Producers; +using RCommon.MediatR; +using RCommon.Models.Events; +using Xunit; + +namespace RCommon.Mediatr.Tests.Bootstrapping; + +public class MultiModuleMediatRTests +{ + [Fact] + public void TwoModules_DistinctProducers_BothRegister() + { + var services = new ServiceCollection(); + + services.AddRCommon().WithEventHandling(eh => + eh.AddProducer()); + services.AddRCommon().WithEventHandling(eh => + eh.AddProducer()); + + services.Count(d => d.ServiceType == typeof(IEventProducer)).Should().Be(2); + } + + [Fact] + public void TwoModules_SameProducer_RegistersOnce() + { + var services = new ServiceCollection(); + + services.AddRCommon().WithEventHandling(eh => + eh.AddProducer()); + services.AddRCommon().WithEventHandling(eh => + eh.AddProducer()); + + services.Count(d => + d.ServiceType == typeof(IEventProducer) && d.ImplementationType == typeof(TestProducerA)) + .Should().Be(1); + } + + public class TestProducerA : IEventProducer + { + public Task ProduceEventAsync(TEvent @event, CancellationToken cancellationToken = default) + where TEvent : ISerializableEvent => Task.CompletedTask; + } + + public class TestProducerB : IEventProducer + { + public Task ProduceEventAsync(TEvent @event, CancellationToken cancellationToken = default) + where TEvent : ISerializableEvent => Task.CompletedTask; + } +} diff --git a/Tests/RCommon.Persistence.Tests/Bootstrapping/DataStoreFactoryOptionsRegisterTests.cs b/Tests/RCommon.Persistence.Tests/Bootstrapping/DataStoreFactoryOptionsRegisterTests.cs new file mode 100644 index 00000000..b430004b --- /dev/null +++ b/Tests/RCommon.Persistence.Tests/Bootstrapping/DataStoreFactoryOptionsRegisterTests.cs @@ -0,0 +1,56 @@ +using System; +using System.Data.Common; +using System.Threading.Tasks; +using FluentAssertions; +using RCommon.Persistence; +using Xunit; + +namespace RCommon.Persistence.Tests.Bootstrapping; + +public class DataStoreFactoryOptionsRegisterTests +{ + [Fact] + public void Register_SameName_SameBase_SameConcrete_IsIdempotent() + { + var options = new DataStoreFactoryOptions(); + + options.Register("DataStoreA"); + Action secondCall = () => options.Register("DataStoreA"); + + secondCall.Should().NotThrow(); + options.Values.Should().HaveCount(1); + } + + [Fact] + public void Register_SameName_SameBase_DifferentConcrete_Throws() + { + var options = new DataStoreFactoryOptions(); + options.Register("DataStoreA"); + + Action act = () => options.Register("DataStoreA"); + + act.Should().Throw() + .WithMessage("*DataStoreA*FakeConcreteA*FakeConcreteB*"); + } + + [Fact] + public void Register_DifferentNames_RegistersBoth() + { + var options = new DataStoreFactoryOptions(); + + options.Register("DataStoreA"); + options.Register("DataStoreB"); + + options.Values.Should().HaveCount(2); + } + + // DataStoreValue's constructor validates concreteType.BaseType == baseType (CLR base class, + // not implemented interface), so fakes must inherit through a concrete abstract class. + public abstract class FakeBase : IDataStore + { + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + public DbConnection GetDbConnection() => throw new NotSupportedException("Test fake"); + } + public class FakeConcreteA : FakeBase { } + public class FakeConcreteB : FakeBase { } +} diff --git a/Tests/RCommon.Persistence.Tests/DataStoreFactoryOptionsTests.cs b/Tests/RCommon.Persistence.Tests/DataStoreFactoryOptionsTests.cs index 4b25a61a..bf59ef22 100644 --- a/Tests/RCommon.Persistence.Tests/DataStoreFactoryOptionsTests.cs +++ b/Tests/RCommon.Persistence.Tests/DataStoreFactoryOptionsTests.cs @@ -37,15 +37,17 @@ public void Register_WithValidTypes_AddsDataStoreValue() } [Fact] - public void Register_WithSameNameAndBaseType_ThrowsUnsupportedDataStoreException() + public void Register_WithSameNameAndBaseTypeButDifferentConcrete_ThrowsUnsupportedDataStoreException() { // Arrange var options = new DataStoreFactoryOptions(); var dataStoreName = "TestDataStore"; options.Register(dataStoreName); - // Act - var action = () => options.Register(dataStoreName); + // Act - registering the same (name, base) with a DIFFERENT concrete should still throw. + // Note: re-registering the same (name, base, concrete) triple is now idempotent; + // see DataStoreFactoryOptionsRegisterTests for the idempotent contract. + var action = () => options.Register(dataStoreName); // Assert action.Should().Throw() @@ -142,6 +144,12 @@ public class TestDataStoreForOptions : TestDataStoreForOptionsBase public override ValueTask DisposeAsync() => ValueTask.CompletedTask; } +public class AnotherConcreteForOptions : TestDataStoreForOptionsBase +{ + public override DbConnection GetDbConnection() => null!; + public override ValueTask DisposeAsync() => ValueTask.CompletedTask; +} + public abstract class AnotherDataStoreForOptionsBase : IDataStore { public virtual DbConnection GetDbConnection() => null!; diff --git a/Tests/RCommon.Stateless.Tests/StatelessDependencyInjectionTests.cs b/Tests/RCommon.Stateless.Tests/StatelessDependencyInjectionTests.cs index e29f5e84..c916b600 100644 --- a/Tests/RCommon.Stateless.Tests/StatelessDependencyInjectionTests.cs +++ b/Tests/RCommon.Stateless.Tests/StatelessDependencyInjectionTests.cs @@ -18,6 +18,9 @@ private class TestRCommonBuilder : IRCommonBuilder public IRCommonBuilder WithCommonFactory() where TService : class where TImplementation : class, TService => this; + public TSubBuilder GetOrAddBuilder(Func factory) + where TSubBuilder : class => factory(); + public string GetBootstrapDiagnostics() => string.Empty; } [Fact] diff --git a/docs/plans/bootstrapping/2026-05-15-bootstrapping.md b/docs/plans/bootstrapping/2026-05-15-bootstrapping.md new file mode 100644 index 00000000..f09cda87 --- /dev/null +++ b/docs/plans/bootstrapping/2026-05-15-bootstrapping.md @@ -0,0 +1,1910 @@ +# Modular Bootstrapper Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make `services.AddRCommon()` and its fluent verbs composable across multiple modules in a single in-process .NET application without breaking existing single-call usage. + +**Architecture:** Cache the `IRCommonBuilder` instance as a `ServiceDescriptor.ImplementationInstance` on the `IServiceCollection`. Add a `GetOrAddBuilder(Func)` helper on `IRCommonBuilder` so every `WithX` extension can route through the cache and skip the redundant `Activator.CreateInstance` call when `T` is already configured. Replace bare `_guidConfigured`/`_dateTimeConfigured` flags with `SingletonRegistration` structs that track impl type for same-type-idempotent / different-type-throw semantics. Wire the existing duplicate-descriptor scanner to run automatically at host startup via an internal `IHostedService`. Preserve `UnsupportedDataStoreException` for backward compat. + +**Tech Stack:** .NET 8 / 9 / 10 multi-target; `Microsoft.Extensions.DependencyInjection`; `Microsoft.Extensions.Hosting.IHostedService`; `Microsoft.Extensions.Logging`. Test framework: xUnit + FluentAssertions (matches existing test conventions). Working branch: `feature/modular-bootstrapper`. + +**Reference documents:** +- Domain spec: [`docs/specs/bootstrapping/bootstrapping.md`](../../specs/bootstrapping/bootstrapping.md) — testable contract. +- Design doc: [`docs/superpowers/specs/2026-05-15-modular-bootstrapper-design.md`](../../superpowers/specs/2026-05-15-modular-bootstrapper-design.md) — implementation specifics, conflict matrix, full call-site enumeration. + +**TDD ordering (from spec):** core idempotency → cache helper → singleton tracker → datastore-name collision → soft-duplicate finalize → per-provider migrations. + +--- + +## File Structure + +### New files + +| Path | Responsibility | +|---|---| +| `Src/RCommon.Core/SingletonRegistration.cs` | Mutable struct tracking `(Configured, ImplType)` for singleton-style verbs. | +| `Src/RCommon.Core/RCommonBootstrapDiagnosticsHostedService.cs` | `IHostedService` that runs the duplicate scanner once at startup. | +| `Tests/RCommon.Core.Tests/Bootstrapping/AddRCommonIdempotencyTests.cs` | Top-level `AddRCommon()` idempotency. | +| `Tests/RCommon.Core.Tests/Bootstrapping/SubBuilderCacheTests.cs` | `GetOrAddBuilder` semantics. | +| `Tests/RCommon.Core.Tests/Bootstrapping/SingletonVerbConflictTests.cs` | Singleton-verb conflict / idempotency. | +| `Tests/RCommon.Core.Tests/Bootstrapping/SoftDuplicateDiagnosticsTests.cs` | Finalize warning emission. | +| `Tests/RCommon.Core.Tests/Bootstrapping/EventProducerDedupTests.cs` | `AddProducer` descriptor-scan dedup. | +| `Tests/RCommon.Persistence.Tests/Bootstrapping/DataStoreFactoryOptionsRegisterTests.cs` | `Register<,>` collision detection. | +| `Tests/RCommon.EfCore.Tests/Bootstrapping/MultiModuleEFCoreTests.cs` | End-to-end multi-module persistence integration. | +| `Tests/RCommon.Mediatr.Tests/Bootstrapping/MultiModuleMediatRTests.cs` | End-to-end multi-module event-handling integration. | +| `Tests/RCommon.Json.Tests/Bootstrapping/JsonSerializationSingletonTests.cs` | `WithJsonSerialization` singleton-style semantics. | + +### Modified files (core) + +| Path | Change | +|---|---| +| `Src/RCommon.Core/IRCommonBuilder.cs` | Add `GetOrAddBuilder(Func)` and `GetBootstrapDiagnostics()`. | +| `Src/RCommon.Core/RCommonBuilder.cs` | Sub-builder cache, `SingletonRegistration` for guid/datetime, relaxed verbs, diagnostics retrieval. | +| `Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs` | `AddRCommon()` cache-lookup-and-return, `IsRCommonInitialized()`, register hosted service. | +| `Src/RCommon.Core/EventHandling/EventHandlingBuilderExtensions.cs` | `WithEventHandling` routes through `GetOrAddBuilder`. `AddProducer` descriptor-scan dedup. | +| `Src/RCommon.Persistence/PersistenceBuilderExtensions.cs` | `WithPersistence` (4 overloads), `WithUnitOfWork` route through `GetOrAddBuilder`. `WithEventTracking` switches to `TryAddScoped`. | +| `Src/RCommon.Persistence/DataStoreFactoryOptions.cs` | `Register` accepts identical `(name, B, C)` as idempotent; throws on `(name, B)` with different `C`. | + +### Modified files (per-provider migrations) + +| Path | Verb(s) affected | +|---|---| +| `Src/RCommon.Mediator/MediatorBuilderExtensions.cs` | `WithMediator` (2 overloads) | +| `Src/RCommon.Mediatr/MediatREventHandlingBuilderExtensions.cs` | `WithEventHandling` (3 overloads) | +| `Src/RCommon.MassTransit/MassTransitEventHandlingBuilderExtensions.cs` | `WithEventHandling` (2 overloads) | +| `Src/RCommon.Caching/CachingBuilderExtensions.cs` | `WithMemoryCaching` (2 overloads), `WithDistributedCaching` (2 overloads) | +| `Src/RCommon.Json/JsonBuilderExtensions.cs` | `WithJsonSerialization` (6 overloads) — singleton-style | +| `Src/RCommon.ApplicationServices/CqrsBuilderExtensions.cs` | `WithCQRS` (2 overloads) | +| `Src/RCommon.ApplicationServices/ValidationBuilderExtensions.cs` | `WithValidation` (2 overloads) | +| `Src/RCommon.FluentValidation/ValidationBuilderExtensions.cs` | `WithValidation` (2 overloads) | +| `Src/RCommon.Blobs/BlobStorageBuilderExtensions.cs` | `WithBlobStorage` (2 overloads) | +| `Src/RCommon.MultiTenancy/MultiTenancyBuilderExtensions.cs` | `WithMultiTenancy` | +| `Src/RCommon.Stateless/StatelessBuilderExtensions.cs` | `WithStatelessStateMachine` (TryAdd) | +| `Src/RCommon.MassTransit.StateMachines/MassTransitStateMachineBuilderExtensions.cs` | `WithMassTransitStateMachine` (TryAdd) | +| `Src/RCommon.Emailing/EmailingBuilderExtensions.cs` | `WithSmtpEmailServices` (TryAdd + singleton-style) | +| `Src/RCommon.SendGrid/SendGridEmailingConfigurationExtensions.cs` | `WithSendGridEmailServices` (TryAdd + singleton-style) | +| `Src/RCommon.Security/SecurityConfigurationExtensions.cs` | `WithClaimsAndPrincipalAccessor` (TryAdd) | +| `Src/RCommon.Web/WebConfigurationExtensions.cs` | `WithClaimsAndPrincipalAccessorForWeb` (TryAdd) | + +### Modified existing tests + +| Path | Reason | +|---|---| +| `Tests/RCommon.Core.Tests/RCommonBuilderTests.cs` | Replace `*CalledTwice_Throws*` tests for `WithSequentialGuidGenerator`, `WithSimpleGuidGenerator`, `WithDateTimeSystem` with idempotent assertions. Add same-type-idempotent and different-type-throws assertions. | + +--- + +## Conventions + +- All tests use **xUnit** with `[Fact]`/`[Theory]` and **FluentAssertions** (`x.Should().Be(...)`), matching existing test files. +- All tests live under `Tests/.Tests/Bootstrapping/` subfolder (new) using the parent namespace. +- Run all tests from the repo root: `dotnet test`. +- Run a single test class: `dotnet test --filter "FullyQualifiedName~AddRCommonIdempotencyTests"`. +- Build the whole solution: `dotnet build Src/RCommon.sln`. +- Branch: `feature/modular-bootstrapper` (already checked out and contains the committed spec + design doc). + +**Commit cadence:** one commit per red-green-refactor cycle. Commit message format: `: ` (e.g., `feat: cache IRCommonBuilder on IServiceCollection`). Never use the Claude signature per CLAUDE.md. + +--- + +## Task 1: AddRCommon idempotency — failing tests + +**Files:** +- Create: `Tests/RCommon.Core.Tests/Bootstrapping/AddRCommonIdempotencyTests.cs` + +- [ ] **Step 1.1: Create directory and test file** + +```bash +mkdir -p Tests/RCommon.Core.Tests/Bootstrapping +``` + +Create `Tests/RCommon.Core.Tests/Bootstrapping/AddRCommonIdempotencyTests.cs`: + +```csharp +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using RCommon.EventHandling; +using RCommon.EventHandling.Producers; +using Xunit; + +namespace RCommon.Core.Tests.Bootstrapping; + +public class AddRCommonIdempotencyTests +{ + [Fact] + public void AddRCommon_CalledTwice_ReturnsSameBuilderInstance() + { + var services = new ServiceCollection(); + + var first = services.AddRCommon(); + var second = services.AddRCommon(); + + second.Should().BeSameAs(first); + } + + [Fact] + public void AddRCommon_CalledTwice_RegistersEventBusOnlyOnce() + { + var services = new ServiceCollection(); + + services.AddRCommon(); + services.AddRCommon(); + + services.Count(d => d.ServiceType == typeof(IEventBus)).Should().Be(1); + } + + [Fact] + public void AddRCommon_CalledTwice_RegistersEventSubscriptionManagerOnlyOnce() + { + var services = new ServiceCollection(); + + services.AddRCommon(); + services.AddRCommon(); + + services.Count(d => d.ServiceType == typeof(EventSubscriptionManager)).Should().Be(1); + } + + [Fact] + public void AddRCommon_CalledTwice_RegistersEventRouterOnlyOnce() + { + var services = new ServiceCollection(); + + services.AddRCommon(); + services.AddRCommon(); + + services.Count(d => d.ServiceType == typeof(IEventRouter)).Should().Be(1); + } + + [Fact] + public void AddRCommon_CalledOnce_HasIdenticalDescriptorCountToCalledOnce() + { + var servicesA = new ServiceCollection(); + var servicesB = new ServiceCollection(); + + servicesA.AddRCommon(); + servicesB.AddRCommon(); + servicesB.AddRCommon(); + + servicesB.Count.Should().Be(servicesA.Count); + } + + [Fact] + public void IsRCommonInitialized_BeforeAddRCommon_ReturnsFalse() + { + var services = new ServiceCollection(); + + services.IsRCommonInitialized().Should().BeFalse(); + } + + [Fact] + public void IsRCommonInitialized_AfterAddRCommon_ReturnsTrue() + { + var services = new ServiceCollection(); + + services.AddRCommon(); + + services.IsRCommonInitialized().Should().BeTrue(); + } +} +``` + +- [ ] **Step 1.2: Run tests, verify they fail to compile / fail with no `IsRCommonInitialized`** + +```bash +dotnet test Tests/RCommon.Core.Tests/RCommon.Core.Tests.csproj --filter "FullyQualifiedName~AddRCommonIdempotencyTests" +``` + +Expected: BUILD FAILURE — `IsRCommonInitialized` extension does not exist; second `AddRCommon` does not return the same instance. + +## Task 2: AddRCommon idempotency — implementation + +**Files:** +- Modify: `Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs` + +- [ ] **Step 2.1: Update `AddRCommon` to cache and return existing builder** + +Replace the `AddRCommon` method body with: + +```csharp +public static IRCommonBuilder AddRCommon(this IServiceCollection services) +{ + var existing = services.FirstOrDefault(d => d.ServiceType == typeof(IRCommonBuilder)); + if (existing?.ImplementationInstance is IRCommonBuilder cached) + { + return cached; + } + + var config = new RCommonBuilder(services); + services.AddSingleton(config); + config.Configure(); // No-op in the base class (just returns Services); preserved for consistency with the existing API shape and in case a subclass overrides it. + return config; +} +``` + +- [ ] **Step 2.2: Add `IsRCommonInitialized` extension** + +Append to the same class (inside `ServiceCollectionExtensions`): + +```csharp +/// +/// Returns true if has been invoked against this collection. +/// +public static bool IsRCommonInitialized(this IServiceCollection services) +{ + return services.Any(d => d.ServiceType == typeof(IRCommonBuilder)); +} +``` + +- [ ] **Step 2.3: Run tests, verify they pass** + +```bash +dotnet test Tests/RCommon.Core.Tests/RCommon.Core.Tests.csproj --filter "FullyQualifiedName~AddRCommonIdempotencyTests" +``` + +Expected: All 7 tests PASS. + +- [ ] **Step 2.4: Commit** + +```bash +git add Tests/RCommon.Core.Tests/Bootstrapping/AddRCommonIdempotencyTests.cs Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs +git commit -m "feat(bootstrapping): cache IRCommonBuilder on IServiceCollection for idempotent AddRCommon" +``` + +--- + +## Task 3: GetOrAddBuilder helper — failing tests + +**Files:** +- Create: `Tests/RCommon.Core.Tests/Bootstrapping/SubBuilderCacheTests.cs` + +- [ ] **Step 3.1: Create test file** + +```csharp +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace RCommon.Core.Tests.Bootstrapping; + +public class SubBuilderCacheTests +{ + [Fact] + public void GetOrAddBuilder_FirstCall_InvokesFactoryOnce() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + var factoryCount = 0; + + builder.GetOrAddBuilder(() => + { + factoryCount++; + return new TestSubBuilder(services); + }); + + factoryCount.Should().Be(1); + } + + [Fact] + public void GetOrAddBuilder_SecondCallForSameType_DoesNotInvokeFactory() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + var factoryCount = 0; + + builder.GetOrAddBuilder(() => { factoryCount++; return new TestSubBuilder(services); }); + builder.GetOrAddBuilder(() => { factoryCount++; return new TestSubBuilder(services); }); + + factoryCount.Should().Be(1); + } + + [Fact] + public void GetOrAddBuilder_SecondCallForSameType_ReturnsCachedInstance() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + var first = builder.GetOrAddBuilder(() => new TestSubBuilder(services)); + var second = builder.GetOrAddBuilder(() => new TestSubBuilder(services)); + + second.Should().BeSameAs(first); + } + + [Fact] + public void GetOrAddBuilder_DifferentTypes_ReturnsDistinctInstances() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + var subA = builder.GetOrAddBuilder(() => new TestSubBuilder(services)); + var subB = builder.GetOrAddBuilder(() => new OtherTestSubBuilder(services)); + + ((object)subA).Should().NotBeSameAs(subB); + } + + private sealed class TestSubBuilder + { + public TestSubBuilder(IServiceCollection services) { } + } + + private sealed class OtherTestSubBuilder + { + public OtherTestSubBuilder(IServiceCollection services) { } + } +} +``` + +- [ ] **Step 3.2: Run, verify build failure (GetOrAddBuilder doesn't exist)** + +```bash +dotnet test Tests/RCommon.Core.Tests/RCommon.Core.Tests.csproj --filter "FullyQualifiedName~SubBuilderCacheTests" +``` + +Expected: BUILD FAILURE — `GetOrAddBuilder` not defined on `IRCommonBuilder`. + +## Task 4: GetOrAddBuilder helper — implementation + +**Files:** +- Modify: `Src/RCommon.Core/IRCommonBuilder.cs` +- Modify: `Src/RCommon.Core/RCommonBuilder.cs` + +- [ ] **Step 4.1: Add interface member** + +Add to `IRCommonBuilder` (after existing members): + +```csharp +/// +/// Returns the cached sub-builder for if one exists, +/// otherwise invokes , caches the result, and returns it. +/// +/// Concrete sub-builder type (e.g., EFCorePerisistenceBuilder). +/// Parameterless factory invoked exactly once per +/// per . Callers close over whichever constructor argument the sub-builder +/// requires — typically builder.Services or builder itself. +TSubBuilder GetOrAddBuilder(Func factory) + where TSubBuilder : class; +``` + +- [ ] **Step 4.2: Add implementation in `RCommonBuilder`** + +Add a private cache field: + +```csharp +private readonly Dictionary _subBuilderCache = new(); +``` + +(Add `using System.Collections.Generic;` if not already present.) + +Add the method body: + +```csharp +public TSubBuilder GetOrAddBuilder(Func factory) + where TSubBuilder : class +{ + if (_subBuilderCache.TryGetValue(typeof(TSubBuilder), out var cached)) + { + return (TSubBuilder)cached; + } + + var built = factory(); + _subBuilderCache[typeof(TSubBuilder)] = built; + return built; +} +``` + +- [ ] **Step 4.3: Run tests, verify they pass** + +```bash +dotnet test Tests/RCommon.Core.Tests/RCommon.Core.Tests.csproj --filter "FullyQualifiedName~SubBuilderCacheTests" +``` + +Expected: All 4 tests PASS. + +- [ ] **Step 4.4: Commit** + +```bash +git add Src/RCommon.Core/IRCommonBuilder.cs Src/RCommon.Core/RCommonBuilder.cs Tests/RCommon.Core.Tests/Bootstrapping/SubBuilderCacheTests.cs +git commit -m "feat(bootstrapping): add GetOrAddBuilder helper for sub-builder caching" +``` + +--- + +## Task 5: SingletonRegistration tracker — failing tests + +**Files:** +- Create: `Tests/RCommon.Core.Tests/Bootstrapping/SingletonVerbConflictTests.cs` + +- [ ] **Step 5.1: Create test file** + +```csharp +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace RCommon.Core.Tests.Bootstrapping; + +public class SingletonVerbConflictTests +{ + [Fact] + public void WithSimpleGuidGenerator_CalledTwice_IsIdempotent() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + builder.WithSimpleGuidGenerator(); + Action secondCall = () => builder.WithSimpleGuidGenerator(); + + secondCall.Should().NotThrow(); + services.Count(d => d.ServiceType == typeof(IGuidGenerator)).Should().Be(1); + } + + [Fact] + public void WithSequentialGuidGenerator_CalledTwice_IsIdempotent() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + builder.WithSequentialGuidGenerator(o => { }); + Action secondCall = () => builder.WithSequentialGuidGenerator(o => { }); + + secondCall.Should().NotThrow(); + services.Count(d => d.ServiceType == typeof(IGuidGenerator)).Should().Be(1); + } + + [Fact] + public void WithSimpleGuidGenerator_AfterSequentialGuidGenerator_ThrowsRCommonBuilderException() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + builder.WithSequentialGuidGenerator(o => { }); + + Action act = () => builder.WithSimpleGuidGenerator(); + + act.Should().Throw() + .WithMessage("*SequentialGuidGenerator*SimpleGuidGenerator*"); + } + + [Fact] + public void WithSequentialGuidGenerator_AfterSimpleGuidGenerator_ThrowsRCommonBuilderException() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + builder.WithSimpleGuidGenerator(); + + Action act = () => builder.WithSequentialGuidGenerator(o => { }); + + act.Should().Throw() + .WithMessage("*SimpleGuidGenerator*SequentialGuidGenerator*"); + } + + [Fact] + public void WithDateTimeSystem_CalledTwice_IsIdempotent() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + builder.WithDateTimeSystem(o => { }); + Action secondCall = () => builder.WithDateTimeSystem(o => { }); + + secondCall.Should().NotThrow(); + services.Count(d => d.ServiceType == typeof(ISystemTime)).Should().Be(1); + } +} +``` + +- [ ] **Step 5.2: Run tests, observe failures** + +```bash +dotnet test Tests/RCommon.Core.Tests/RCommon.Core.Tests.csproj --filter "FullyQualifiedName~SingletonVerbConflictTests" +``` + +Expected: Several tests FAIL — current behavior throws on any second call regardless of type. + +## Task 6: SingletonRegistration tracker — implementation + +**Files:** +- Create: `Src/RCommon.Core/SingletonRegistration.cs` +- Modify: `Src/RCommon.Core/RCommonBuilder.cs` + +- [ ] **Step 6.1: Create the SingletonRegistration struct** + +`Src/RCommon.Core/SingletonRegistration.cs`: + +```csharp +using System; + +namespace RCommon +{ + /// + /// Tracks whether a singleton-style RCommon verb has been configured and which implementation + /// type was chosen. Used by verbs like WithSimpleGuidGenerator and WithDateTimeSystem + /// to enforce same-type-idempotent / different-type-throw semantics across modular calls. + /// + internal struct SingletonRegistration + { + public bool Configured; + public Type? ImplementationType; + } +} +``` + +- [ ] **Step 6.2: Replace bool flags in `RCommonBuilder` with `SingletonRegistration` fields** + +In `Src/RCommon.Core/RCommonBuilder.cs`, replace: + +```csharp +private bool _guidConfigured = false; +private bool _dateTimeConfigured = false; +``` + +with: + +```csharp +private SingletonRegistration _guidRegistration; +private SingletonRegistration _dateTimeRegistration; +``` + +- [ ] **Step 6.3: Rewrite `WithSequentialGuidGenerator` for new semantics** + +```csharp +public IRCommonBuilder WithSequentialGuidGenerator(Action actions) +{ + if (_guidRegistration.Configured) + { + if (_guidRegistration.ImplementationType == typeof(SequentialGuidGenerator)) + { + // Same impl re-registered: idempotent; just append the options delegate + this.Services.Configure(actions); + return this; + } + throw new RCommonBuilderException( + $"IGuidGenerator already configured as '{_guidRegistration.ImplementationType?.FullName}'; " + + $"cannot reconfigure as '{typeof(SequentialGuidGenerator).FullName}'. " + + "To configure multiple modules consistently, ensure all modules agree on the same IGuidGenerator implementation, " + + "or designate a single composition root that performs this registration."); + } + + this.Services.Configure(actions); + this.Services.AddTransient(); + _guidRegistration = new SingletonRegistration { Configured = true, ImplementationType = typeof(SequentialGuidGenerator) }; + return this; +} +``` + +- [ ] **Step 6.4: Rewrite `WithSimpleGuidGenerator` for new semantics** + +```csharp +public IRCommonBuilder WithSimpleGuidGenerator() +{ + if (_guidRegistration.Configured) + { + if (_guidRegistration.ImplementationType == typeof(SimpleGuidGenerator)) + { + return this; + } + throw new RCommonBuilderException( + $"IGuidGenerator already configured as '{_guidRegistration.ImplementationType?.FullName}'; " + + $"cannot reconfigure as '{typeof(SimpleGuidGenerator).FullName}'. " + + "To configure multiple modules consistently, ensure all modules agree on the same IGuidGenerator implementation, " + + "or designate a single composition root that performs this registration."); + } + + this.Services.AddScoped(); + _guidRegistration = new SingletonRegistration { Configured = true, ImplementationType = typeof(SimpleGuidGenerator) }; + return this; +} +``` + +- [ ] **Step 6.5: Rewrite `WithDateTimeSystem` for new semantics** + +```csharp +public IRCommonBuilder WithDateTimeSystem(Action actions) +{ + if (_dateTimeRegistration.Configured) + { + // Only one impl type exists; always idempotent. Still append the options delegate so + // additional configuration accumulates per Options pattern. + this.Services.Configure(actions); + return this; + } + + this.Services.Configure(actions); + this.Services.AddTransient(); + _dateTimeRegistration = new SingletonRegistration { Configured = true, ImplementationType = typeof(SystemTime) }; + return this; +} +``` + +- [ ] **Step 6.6: Run new tests, verify pass** + +```bash +dotnet test Tests/RCommon.Core.Tests/RCommon.Core.Tests.csproj --filter "FullyQualifiedName~SingletonVerbConflictTests" +``` + +Expected: All 5 tests PASS. + +- [ ] **Step 6.7: Update existing `RCommonBuilderTests` to reflect relaxed semantics** + +In `Tests/RCommon.Core.Tests/RCommonBuilderTests.cs`: + +- Delete `WithSequentialGuidGenerator_CalledTwice_ThrowsRCommonBuilderException` (lines ~134-147). +- Delete `WithSimpleGuidGenerator_CalledTwice_ThrowsRCommonBuilderException` (lines ~186-198). +- Delete `WithDateTimeSystem_CalledTwice_ThrowsRCommonBuilderException` (lines ~273-286). +- Keep `WithSimpleGuidGenerator_AfterSequentialGuidGenerator_ThrowsRCommonBuilderException` — still valid. + +- [ ] **Step 6.8: Run the full RCommon.Core.Tests project to confirm no regressions** + +```bash +dotnet test Tests/RCommon.Core.Tests/RCommon.Core.Tests.csproj +``` + +Expected: All tests PASS. + +- [ ] **Step 6.9: Commit** + +```bash +git add Src/RCommon.Core/SingletonRegistration.cs Src/RCommon.Core/RCommonBuilder.cs Tests/RCommon.Core.Tests/Bootstrapping/SingletonVerbConflictTests.cs Tests/RCommon.Core.Tests/RCommonBuilderTests.cs +git commit -m "feat(bootstrapping): same-type idempotent, different-type throw for singleton verbs" +``` + +--- + +## Task 7: DataStoreFactoryOptions collision detection — failing tests + +**Files:** +- Create: `Tests/RCommon.Persistence.Tests/Bootstrapping/DataStoreFactoryOptionsRegisterTests.cs` + +- [ ] **Step 7.1: Create test file** + +```csharp +using System; +using System.Data.Common; +using System.Threading.Tasks; +using FluentAssertions; +using RCommon.Persistence; +using Xunit; + +namespace RCommon.Persistence.Tests.Bootstrapping; + +public class DataStoreFactoryOptionsRegisterTests +{ + [Fact] + public void Register_SameName_SameBase_SameConcrete_IsIdempotent() + { + var options = new DataStoreFactoryOptions(); + + options.Register("DataStoreA"); + Action secondCall = () => options.Register("DataStoreA"); + + secondCall.Should().NotThrow(); + options.Values.Should().HaveCount(1); + } + + [Fact] + public void Register_SameName_SameBase_DifferentConcrete_Throws() + { + var options = new DataStoreFactoryOptions(); + options.Register("DataStoreA"); + + Action act = () => options.Register("DataStoreA"); + + act.Should().Throw() + .WithMessage("*DataStoreA*FakeConcreteA*FakeConcreteB*"); + } + + [Fact] + public void Register_DifferentNames_RegistersBoth() + { + var options = new DataStoreFactoryOptions(); + + options.Register("DataStoreA"); + options.Register("DataStoreB"); + + options.Values.Should().HaveCount(2); + } + + // DataStoreValue's constructor validates concreteType.BaseType == baseType (CLR base class, + // not implemented interface), so fakes must inherit through a concrete abstract class. + public abstract class FakeBase : IDataStore + { + public ValueTask DisposeAsync() => ValueTask.CompletedTask; + public DbConnection GetDbConnection() => throw new NotSupportedException("Test fake"); + } + public class FakeConcreteA : FakeBase { } + public class FakeConcreteB : FakeBase { } +} +``` + +- [ ] **Step 7.2: Run, verify failure** + +```bash +dotnet test Tests/RCommon.Persistence.Tests/RCommon.Persistence.Tests.csproj --filter "FullyQualifiedName~DataStoreFactoryOptionsRegisterTests" +``` + +Expected: First test FAILS — current `Register` throws on any duplicate `(name, B)` regardless of `C`. + +## Task 8: DataStoreFactoryOptions collision detection — implementation + +**Files:** +- Modify: `Src/RCommon.Persistence/DataStoreFactoryOptions.cs` + +- [ ] **Step 8.1: Update Register logic** + +Replace the method body: + +```csharp +public void Register(string name) + where B : IDataStore + where C : IDataStore +{ + var existing = Values.FirstOrDefault(x => x.Name == name && x.BaseType == typeof(B)); + if (existing is null) + { + Values.Add(new DataStoreValue(name, typeof(B), typeof(C))); + return; + } + + if (existing.ConcreteType == typeof(C)) + { + return; + } + + throw new UnsupportedDataStoreException( + $"Data store '{name}' for base type '{typeof(B).GetGenericTypeName()}' is already registered with concrete type " + + $"'{existing.ConcreteType.GetGenericTypeName()}'; cannot reconfigure as '{typeof(C).GetGenericTypeName()}'."); +} +``` + +`DataStoreValue` exposes the concrete type via the `ConcreteType` property (see `Src/RCommon.Persistence/DataStoreValue.cs:50`). + +- [ ] **Step 8.2: Run tests, verify pass** + +```bash +dotnet test Tests/RCommon.Persistence.Tests/RCommon.Persistence.Tests.csproj --filter "FullyQualifiedName~DataStoreFactoryOptionsRegisterTests" +``` + +Expected: All 3 tests PASS. + +- [ ] **Step 8.3: Run full Persistence test project** + +```bash +dotnet test Tests/RCommon.Persistence.Tests/RCommon.Persistence.Tests.csproj +``` + +Expected: All tests PASS (or any failures are pre-existing — verify by checking `git stash; dotnet test ...; git stash pop`). + +- [ ] **Step 8.4: Commit** + +```bash +git add Src/RCommon.Persistence/DataStoreFactoryOptions.cs Tests/RCommon.Persistence.Tests/Bootstrapping/DataStoreFactoryOptionsRegisterTests.cs +git commit -m "feat(persistence): allow idempotent re-registration of identical datastore mappings" +``` + +--- + +## Task 9: AddProducer descriptor-scan dedup — failing tests + +**Files:** +- Create: `Tests/RCommon.Core.Tests/Bootstrapping/EventProducerDedupTests.cs` + +- [ ] **Step 9.1: Create test file** + +```csharp +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using RCommon.EventHandling; +using RCommon.EventHandling.Producers; +using RCommon.Models.Events; +using Xunit; + +namespace RCommon.Core.Tests.Bootstrapping; + +public class EventProducerDedupTests +{ + [Fact] + public void AddProducer_SameTypeCalledTwice_RegistersOnce() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + builder.WithEventHandling(eh => + { + eh.AddProducer(); + eh.AddProducer(); + }); + + var producerDescriptors = services + .Where(d => d.ServiceType == typeof(IEventProducer) && d.ImplementationType == typeof(TestProducer)) + .ToList(); + producerDescriptors.Should().HaveCount(1); + } + + [Fact] + public void AddProducer_DifferentTypes_RegistersBoth() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + builder.WithEventHandling(eh => + { + eh.AddProducer(); + eh.AddProducer(); + }); + + services.Count(d => d.ServiceType == typeof(IEventProducer)).Should().Be(2); + } + + public class TestEventHandlingBuilder : IEventHandlingBuilder + { + public TestEventHandlingBuilder(IRCommonBuilder builder) { Services = builder.Services; } + public IServiceCollection Services { get; } + } + + public class TestProducer : IEventProducer + { + public Task ProduceEventAsync(TEvent @event, CancellationToken cancellationToken = default) + where TEvent : ISerializableEvent => Task.CompletedTask; + } + + public class OtherTestProducer : IEventProducer + { + public Task ProduceEventAsync(TEvent @event, CancellationToken cancellationToken = default) + where TEvent : ISerializableEvent => Task.CompletedTask; + } +} +``` + +> The `IEventProducer` signature is verified from `Src/RCommon.Core/EventHandling/Producers/IEventProducer.cs`: `Task ProduceEventAsync(TEvent @event, CancellationToken cancellationToken = default) where TEvent : ISerializableEvent`. + +- [ ] **Step 9.2: Run, verify failure** + +```bash +dotnet test Tests/RCommon.Core.Tests/RCommon.Core.Tests.csproj --filter "FullyQualifiedName~EventProducerDedupTests" +``` + +Expected: `AddProducer_SameTypeCalledTwice_RegistersOnce` FAILS — current `AddSingleton` registers twice. + +## Task 10: AddProducer descriptor-scan dedup — implementation + +**Files:** +- Modify: `Src/RCommon.Core/EventHandling/EventHandlingBuilderExtensions.cs` + +- [ ] **Step 10.1: Update `AddProducer()` (the parameterless overload)** + +Replace the method body: + +```csharp +public static void AddProducer(this IEventHandlingBuilder builder) + where T : class, IEventProducer +{ + var alreadyRegistered = builder.Services.Any(d => + d.ServiceType == typeof(IEventProducer) && d.ImplementationType == typeof(T)); + if (!alreadyRegistered) + { + builder.Services.AddSingleton(); + } + + var subscriptionManager = builder.Services.GetSubscriptionManager(); + subscriptionManager?.AddProducerForBuilder(builder.GetType(), typeof(T)); +} +``` + +- [ ] **Step 10.2: Update `AddProducer(Func)` (factory overload)** + +For the factory overload, descriptor inspection by `ImplementationType` doesn't work (factory descriptors set `ImplementationFactory`, not `ImplementationType`). Instead, gate on the existing `EventSubscriptionManager`'s tracking — its `AddProducerForBuilder` already uses a `HashSet` per builder type (verified in `Src/RCommon.Core/EventHandling/Producers/EventSubscriptionManager.cs:27-34`), so we just need a `HasProducerForBuilder` lookup method. + +```csharp +public static void AddProducer(this IEventHandlingBuilder builder, Func getProducer) + where T : class, IEventProducer +{ + var subscriptionManager = builder.Services.GetSubscriptionManager(); + var alreadyTracked = subscriptionManager?.HasProducerForBuilder(builder.GetType(), typeof(T)) ?? false; + + if (!alreadyTracked) + { + builder.Services.AddSingleton(getProducer); + } + + subscriptionManager?.AddProducerForBuilder(builder.GetType(), typeof(T)); +} +``` + +Add a new method to `Src/RCommon.Core/EventHandling/Producers/EventSubscriptionManager.cs`: + +```csharp +/// +/// Returns true if the given producer type has already been registered through the given builder type. +/// +public bool HasProducerForBuilder(Type builderType, Type producerType) +{ + if (_builderProducerMap.TryGetValue(builderType, out var producers)) + { + lock (producers) + { + return producers.Contains(producerType); + } + } + return false; +} +``` + +(`AddProducerForBuilder` is already set-based, so calling it twice with the same pair is naturally idempotent — no further change needed there.) + +- [ ] **Step 10.3: Update `AddProducer(T producer)` (instance overload)** + +Already uses `TryAddSingleton`, which is correct here. Verify the producer-for-builder tracking is also idempotent: + +```csharp +public static void AddProducer(this IEventHandlingBuilder builder, T producer) + where T : class, IEventProducer +{ + builder.Services.TryAddSingleton(producer); + builder.Services.TryAddSingleton(sp => sp.GetRequiredService()); + + if (producer is IHostedService service) + { + builder.Services.TryAddSingleton(service); + } + + var subscriptionManager = builder.Services.GetSubscriptionManager(); + subscriptionManager?.AddProducerForBuilder(builder.GetType(), typeof(T)); +} +``` + +(No code change required if the existing version already uses `TryAddSingleton` consistently — confirm by reading the file.) + +- [ ] **Step 10.4: Run new tests, verify pass** + +```bash +dotnet test Tests/RCommon.Core.Tests/RCommon.Core.Tests.csproj --filter "FullyQualifiedName~EventProducerDedupTests" +``` + +Expected: Both tests PASS. + +- [ ] **Step 10.5: Run full Core tests** + +```bash +dotnet test Tests/RCommon.Core.Tests/RCommon.Core.Tests.csproj +``` + +Expected: All tests PASS. + +- [ ] **Step 10.6: Commit** + +```bash +git add Src/RCommon.Core/EventHandling/EventHandlingBuilderExtensions.cs Src/RCommon.Core/EventHandling/Producers/EventSubscriptionManager.cs Tests/RCommon.Core.Tests/Bootstrapping/EventProducerDedupTests.cs +git commit -m "feat(event-handling): descriptor-scan dedup for AddProducer" +``` + +--- + +## Task 11: Route core verbs through GetOrAddBuilder + +**Files:** +- Modify: `Src/RCommon.Core/EventHandling/EventHandlingBuilderExtensions.cs` +- Modify: `Src/RCommon.Persistence/PersistenceBuilderExtensions.cs` + +- [ ] **Step 11.1: Update `WithEventHandling` to use GetOrAddBuilder** + +Replace the body of both overloads (parameterless and `Action`): + +```csharp +public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder) + where T : IEventHandlingBuilder +{ + return WithEventHandling(builder, x => { }); +} + +public static IRCommonBuilder WithEventHandling(this IRCommonBuilder builder, Action actions) + where T : IEventHandlingBuilder +{ + var eventHandlingConfig = builder.GetOrAddBuilder( + () => (T)Activator.CreateInstance(typeof(T), new object[] { builder })!); + actions(eventHandlingConfig); + return builder; +} +``` + +- [ ] **Step 11.2: Update `WithPersistence` (current overloads)** + +Replace the body of `WithPersistence(builder, Action)`: + +```csharp +public static IRCommonBuilder WithPersistence(this IRCommonBuilder builder, Action objectAccessActions) + where TObjectAccess : IPersistenceBuilder +{ + builder.Services.TryAddTransient(); + + var dataConfiguration = builder.GetOrAddBuilder( + () => (TObjectAccess)Activator.CreateInstance(typeof(TObjectAccess), new object[] { builder.Services })!); + objectAccessActions(dataConfiguration); + builder = WithEventTracking(builder); + return builder; +} +``` + +- [ ] **Step 11.3: Update `WithUnitOfWork`** + +```csharp +public static IRCommonBuilder WithUnitOfWork(this IRCommonBuilder builder, Action unitOfWorkActions) + where TUnitOfWork : IUnitOfWorkBuilder +{ + var unitOfWorkConfiguration = builder.GetOrAddBuilder( + () => (TUnitOfWork)Activator.CreateInstance(typeof(TUnitOfWork), new object[] { builder.Services })!); + unitOfWorkActions(unitOfWorkConfiguration); + return builder; +} +``` + +- [ ] **Step 11.4: Update `WithEventTracking` to use TryAdd** + +```csharp +private static IRCommonBuilder WithEventTracking(this IRCommonBuilder builder) +{ + builder.Services.TryAddScoped(); + builder.Services.TryAddScoped(); + return builder; +} +``` + +- [ ] **Step 11.5: Update the four deprecated `WithPersistence` overloads** + +Apply the same `GetOrAddBuilder` routing to all four `[Obsolete]` overloads (at `Src/RCommon.Persistence/PersistenceBuilderExtensions.cs` lines 94, 111, 129, 148) so legacy users don't regress. + +- [ ] **Step 11.6: Build the solution to surface any consumers that broke** + +```bash +dotnet build Src/RCommon.sln +``` + +Expected: BUILD SUCCESS. No new errors. + +- [ ] **Step 11.7: Run full Core and Persistence test suites** + +```bash +dotnet test Tests/RCommon.Core.Tests/RCommon.Core.Tests.csproj +dotnet test Tests/RCommon.Persistence.Tests/RCommon.Persistence.Tests.csproj +``` + +Expected: All tests PASS. + +- [ ] **Step 11.8: Commit** + +```bash +git add Src/RCommon.Core/EventHandling/EventHandlingBuilderExtensions.cs Src/RCommon.Persistence/PersistenceBuilderExtensions.cs +git commit -m "feat(bootstrapping): route core verbs through GetOrAddBuilder cache" +``` + +--- + +## Task 12: WithJsonSerialization singleton-style — failing tests + +**Files:** +- Create: `Tests/RCommon.Json.Tests/Bootstrapping/JsonSerializationSingletonTests.cs` + +- [ ] **Step 12.1: Read existing test base and verify Json.Tests has access to JsonNetBuilder + TextJsonBuilder** + +```bash +ls Tests/RCommon.JsonNet.Tests Tests/RCommon.SystemTextJson.Tests +``` + +If `RCommon.Json.Tests` doesn't reference both builders, place this test file in `Tests/RCommon.JsonNet.Tests/Bootstrapping/` instead and reference both packages as project references (add to the .csproj). + +- [ ] **Step 12.2: Create test file** + +```csharp +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using RCommon.JsonNet; +using RCommon.SystemTextJson; +using Xunit; + +namespace RCommon.JsonNet.Tests.Bootstrapping; + +public class JsonSerializationSingletonTests +{ + [Fact] + public void WithJsonSerialization_SameType_IsIdempotent() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + + builder.WithJsonSerialization(); + Action secondCall = () => builder.WithJsonSerialization(); + + secondCall.Should().NotThrow(); + } + + [Fact] + public void WithJsonSerialization_SameType_BothActionsApplied() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + var firstActionRan = false; + var secondActionRan = false; + + builder.WithJsonSerialization(b => firstActionRan = true); + builder.WithJsonSerialization(b => secondActionRan = true); + + firstActionRan.Should().BeTrue(); + secondActionRan.Should().BeTrue(); + } + + [Fact] + public void WithJsonSerialization_DifferentType_Throws() + { + var services = new ServiceCollection(); + var builder = services.AddRCommon(); + builder.WithJsonSerialization(); + + Action act = () => builder.WithJsonSerialization(); + + act.Should().Throw() + .WithMessage("*JsonNetBuilder*TextJsonBuilder*"); + } +} +``` + +- [ ] **Step 12.3: Run, verify failure** + +```bash +dotnet test Tests/RCommon.JsonNet.Tests/RCommon.JsonNet.Tests.csproj --filter "FullyQualifiedName~JsonSerializationSingletonTests" +``` + +Expected: Tests FAIL — current impl creates duplicate descriptors. + +## Task 13: WithJsonSerialization singleton-style — implementation + +**Files:** +- Modify: `Src/RCommon.Json/JsonBuilderExtensions.cs` +- Modify: `Src/RCommon.Core/RCommonBuilder.cs` (add `_jsonRegistration`) +- Modify: `Src/RCommon.Core/IRCommonBuilder.cs` (if a public helper is needed; otherwise keep internal via a static API on the builder) + +- [ ] **Step 13.1: Add an internal `TryRegisterJsonImplementation(Type)` helper** + +The cleanest way to expose singleton-tracking to non-core packages without adding to `IRCommonBuilder` is to use the `GetOrAddBuilder` cache itself as the singleton check (since each concrete `T` gets one cache slot). If a different `T` is registered, throwing requires detecting a conflict before `GetOrAddBuilder` is called. + +Approach: Introduce a `WithJsonSerializationCheck` private helper in `JsonBuilderExtensions.cs` that scans `builder.Services` for `IJsonSerializer`/`IJsonBuilder`-registered descriptors and throws if the concrete impl differs. Use `GetOrAddBuilder` for the cache. + +Actually simpler: use the existence of *any* cached `IJsonBuilder` implementation in the builder's sub-builder cache as the conflict signal. Add an internal method to `RCommonBuilder`: + +```csharp +// In RCommonBuilder.cs +internal Type? TryGetCachedSubBuilderImplementing(Type interfaceType) +{ + foreach (var key in _subBuilderCache.Keys) + { + if (interfaceType.IsAssignableFrom(key)) + { + return key; + } + } + return null; +} +``` + +Expose via a static helper class in `Src/RCommon.Core/Bootstrapping/RCommonBuilderInternals.cs`: + +```csharp +namespace RCommon.Bootstrapping +{ + /// + /// Internal helpers for singleton-style WithX verbs that live outside RCommon.Core. + /// Not intended for use by application code. + /// + public static class RCommonBuilderInternals + { + public static Type? FindCachedImplementationOf(IRCommonBuilder builder) + { + return (builder as RCommonBuilder)?.TryGetCachedSubBuilderImplementing(typeof(TInterface)); + } + } +} +``` + +- [ ] **Step 13.2: Update `WithJsonSerialization` (primary 4-arg overload)** + +```csharp +public static IRCommonBuilder WithJsonSerialization(this IRCommonBuilder builder, + Action serializeOptions, + Action deSerializeOptions, + Action actions) + where T : IJsonBuilder +{ + Guard.IsNotNull(serializeOptions, nameof(serializeOptions)); + Guard.IsNotNull(deSerializeOptions, nameof(deSerializeOptions)); + Guard.IsNotNull(actions, nameof(actions)); + + var existing = RCommonBuilderInternals.FindCachedImplementationOf(builder); + if (existing is not null && existing != typeof(T)) + { + throw new RCommonBuilderException( + $"IJsonBuilder already configured as '{existing.FullName}'; " + + $"cannot reconfigure as '{typeof(T).FullName}'. " + + "To configure multiple modules consistently, ensure all modules agree on the same JSON serialization implementation."); + } + + builder.Services.Configure(serializeOptions); + // NOTE: deSerializeOptions is intentionally not wired up here. The existing implementation + // (Src/RCommon.Json/JsonBuilderExtensions.cs:112) also does not call Configure. + // Preserving that pre-existing behavior keeps this change scope-limited to the singleton-style + // migration; the missing wiring is tracked separately (or in a follow-up). + + var jsonConfig = builder.GetOrAddBuilder( + () => (T)Activator.CreateInstance(typeof(T), new object[] { builder })!); + actions(jsonConfig); + return builder; +} +``` + +(The five other overloads delegate to this one, so they pick up the new behavior automatically.) + +- [ ] **Step 13.3: Run tests, verify pass** + +```bash +dotnet test Tests/RCommon.JsonNet.Tests/RCommon.JsonNet.Tests.csproj --filter "FullyQualifiedName~JsonSerializationSingletonTests" +``` + +Expected: All 3 tests PASS. + +- [ ] **Step 13.4: Run all Json-related test projects** + +```bash +dotnet test Tests/RCommon.Json.Tests/RCommon.Json.Tests.csproj +dotnet test Tests/RCommon.JsonNet.Tests/RCommon.JsonNet.Tests.csproj +dotnet test Tests/RCommon.SystemTextJson.Tests/RCommon.SystemTextJson.Tests.csproj +``` + +Expected: All PASS. + +- [ ] **Step 13.5: Commit** + +```bash +git add Src/RCommon.Json/JsonBuilderExtensions.cs Src/RCommon.Core/RCommonBuilder.cs Src/RCommon.Core/Bootstrapping/RCommonBuilderInternals.cs Tests/RCommon.JsonNet.Tests/Bootstrapping/JsonSerializationSingletonTests.cs +git commit -m "feat(json): singleton-style WithJsonSerialization; same type idempotent, different throws" +``` + +--- + +## Task 14: Per-provider migrations — mechanical edits + +For each provider extension below, replace `Activator.CreateInstance(...)` with `builder.GetOrAddBuilder(() => Activator.CreateInstance(...))`. The factory's constructor argument matches whatever the existing code passes (`builder.Services` for most, `builder` for event-handling-style packages — read each file to determine). + +Each file gets its own commit so the history stays bisectable. + +- [ ] **Step 14.1: `Src/RCommon.Mediator/MediatorBuilderExtensions.cs`** (both `WithMediator` overloads) + +Read the file, then route through `GetOrAddBuilder`. Run `dotnet build Src/RCommon.Mediator/RCommon.Mediator.csproj` and `dotnet test Tests/RCommon.Mediator.Tests/RCommon.Mediator.Tests.csproj` — confirm green. Commit: + +```bash +git add Src/RCommon.Mediator/MediatorBuilderExtensions.cs +git commit -m "feat(mediator): route WithMediator through GetOrAddBuilder cache" +``` + +- [ ] **Step 14.2: `Src/RCommon.Mediatr/MediatREventHandlingBuilderExtensions.cs`** (3 `WithEventHandling` overloads) + +Same pattern. Verify tests, commit: + +```bash +git commit -m "feat(mediatr): route WithEventHandling through GetOrAddBuilder cache" +``` + +- [ ] **Step 14.3: `Src/RCommon.MassTransit/MassTransitEventHandlingBuilderExtensions.cs`** (2 overloads) + +```bash +git commit -m "feat(masstransit): route WithEventHandling through GetOrAddBuilder cache" +``` + +- [ ] **Step 14.4: `Src/RCommon.Caching/CachingBuilderExtensions.cs`** (`WithMemoryCaching` × 2 + `WithDistributedCaching` × 2) + +```bash +git commit -m "feat(caching): route WithMemoryCaching and WithDistributedCaching through cache" +``` + +- [ ] **Step 14.5: `Src/RCommon.ApplicationServices/CqrsBuilderExtensions.cs`** (2 overloads) + +```bash +git commit -m "feat(cqrs): route WithCQRS through GetOrAddBuilder cache" +``` + +- [ ] **Step 14.6: `Src/RCommon.ApplicationServices/ValidationBuilderExtensions.cs`** + `Src/RCommon.FluentValidation/ValidationBuilderExtensions.cs` + +```bash +git commit -m "feat(validation): route WithValidation through GetOrAddBuilder cache" +``` + +- [ ] **Step 14.7: `Src/RCommon.Blobs/BlobStorageBuilderExtensions.cs`** (2 overloads) + +```bash +git commit -m "feat(blobs): route WithBlobStorage through GetOrAddBuilder cache" +``` + +- [ ] **Step 14.8: `Src/RCommon.MultiTenancy/MultiTenancyBuilderExtensions.cs`** (1 overload) + +```bash +git commit -m "feat(multitenancy): route WithMultiTenancy through GetOrAddBuilder cache" +``` + +After each commit, build the full solution and run relevant per-package tests: + +```bash +dotnet build Src/RCommon.sln +dotnet test Tests/.Tests/.Tests.csproj +``` + +Stop and investigate any failure. Do not proceed to the next provider until the current one is green. + +--- + +## Task 15: TryAdd hardening for parameterless verbs + +For verbs that take no generic argument (no sub-builder cache slot needed), make their underlying registrations idempotent by switching `services.AddXxx()` to `services.TryAddXxx()`. + +- [ ] **Step 15.1: `Src/RCommon.Stateless/StatelessBuilderExtensions.cs` (`WithStatelessStateMachine`)** + +Read the file. For each `services.Add*` call, switch to `services.TryAdd*`. Build, test, commit: + +```bash +git commit -m "feat(stateless): idempotent registrations for WithStatelessStateMachine" +``` + +- [ ] **Step 15.2: `Src/RCommon.MassTransit.StateMachines/MassTransitStateMachineBuilderExtensions.cs`** + +```bash +git commit -m "feat(masstransit-sm): idempotent registrations for WithMassTransitStateMachine" +``` + +- [ ] **Step 15.3: `Src/RCommon.Emailing/EmailingBuilderExtensions.cs` + `Src/RCommon.SendGrid/SendGridEmailingConfigurationExtensions.cs`** + +These are singleton-style (only one `IEmailService` makes sense). Switch to `TryAdd*` and add a singleton-conflict guard: if a different `IEmailService` impl already registered, throw `RCommonBuilderException`. Detect via descriptor scan. + +```bash +git commit -m "feat(email): singleton-style With*EmailServices with conflict detection" +``` + +- [ ] **Step 15.4: `Src/RCommon.Security/SecurityConfigurationExtensions.cs` + `Src/RCommon.Web/WebConfigurationExtensions.cs`** + +`TryAdd*` swap. + +```bash +git commit -m "feat(security): idempotent registrations for principal-accessor verbs" +``` + +--- + +## Task 16: Soft-duplicate finalize hosted service — failing tests + +**Files:** +- Create: `Tests/RCommon.Core.Tests/Bootstrapping/SoftDuplicateDiagnosticsTests.cs` + +- [ ] **Step 16.1: Create test file** + +```csharp +using System.Threading; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace RCommon.Core.Tests.Bootstrapping; + +public class SoftDuplicateDiagnosticsTests +{ + [Fact] + public async Task HostedService_WithSoftDuplicates_EmitsSingleWarning() + { + var capturedWarnings = new List(); + var services = new ServiceCollection(); + services.AddSingleton(new TestLoggerFactory(capturedWarnings)); + services.AddLogging(); + + services.AddRCommon(); + // Inject a duplicate registration to trigger soft-duplicate detection + services.AddTransient(); + services.AddTransient(); + + var provider = services.BuildServiceProvider(); + var hostedServices = provider.GetServices(); + foreach (var hs in hostedServices) + { + await hs.StartAsync(CancellationToken.None); + } + + capturedWarnings.Should().HaveCount(1); + capturedWarnings[0].Should().Contain("FakeServiceImpl"); + } + + [Fact] + public async Task HostedService_NoSoftDuplicates_EmitsNoWarning() + { + var capturedWarnings = new List(); + var services = new ServiceCollection(); + services.AddSingleton(new TestLoggerFactory(capturedWarnings)); + services.AddLogging(); + + services.AddRCommon(); + + var provider = services.BuildServiceProvider(); + var hostedServices = provider.GetServices(); + foreach (var hs in hostedServices) + { + await hs.StartAsync(CancellationToken.None); + } + + capturedWarnings.Should().BeEmpty(); + } + + [Fact] + public async Task HostedService_CalledTwice_OnlyRunsScannerOnce() + { + var capturedWarnings = new List(); + var services = new ServiceCollection(); + services.AddSingleton(new TestLoggerFactory(capturedWarnings)); + services.AddLogging(); + + services.AddRCommon(); + services.AddTransient(); + services.AddTransient(); + + var provider = services.BuildServiceProvider(); + var hostedServices = provider.GetServices().ToList(); + foreach (var hs in hostedServices) + { + await hs.StartAsync(CancellationToken.None); + await hs.StartAsync(CancellationToken.None); + } + + capturedWarnings.Should().HaveCount(1); + } + + public interface IFakeService { } + public class FakeServiceImpl : IFakeService { } + + private sealed class TestLoggerFactory : ILoggerFactory + { + private readonly List _warnings; + public TestLoggerFactory(List warnings) { _warnings = warnings; } + public void AddProvider(ILoggerProvider provider) { } + public ILogger CreateLogger(string categoryName) => new TestLogger(_warnings); + public void Dispose() { } + } + + private sealed class TestLogger : ILogger + { + private readonly List _warnings; + public TestLogger(List warnings) { _warnings = warnings; } + public IDisposable? BeginScope(TState state) where TState : notnull => null; + public bool IsEnabled(LogLevel logLevel) => logLevel == LogLevel.Warning; + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (logLevel == LogLevel.Warning) + { + _warnings.Add(formatter(state, exception)); + } + } + } +} +``` + +- [ ] **Step 16.2: Run, verify failure** + +Expected: Tests FAIL — `IHostedService` for diagnostics doesn't exist yet. + +## Task 17: Soft-duplicate finalize hosted service — implementation + +**Files:** +- Create: `Src/RCommon.Core/RCommonBootstrapDiagnosticsHostedService.cs` +- Modify: `Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs` (register the hosted service in `AddRCommon`) +- Modify: `Src/RCommon.Core/RCommonBuilder.cs` (add `_diagnosticsRun` flag and `_bootstrapDiagnostics` field + `GetBootstrapDiagnostics()`) +- Modify: `Src/RCommon.Core/IRCommonBuilder.cs` (add `GetBootstrapDiagnostics()`) + +- [ ] **Step 17.1: Add `_diagnosticsRun` / `_bootstrapDiagnostics` to `RCommonBuilder`** + +```csharp +private bool _diagnosticsRun; +private string _bootstrapDiagnostics = string.Empty; + +internal bool TrySetDiagnosticsRun() +{ + if (_diagnosticsRun) return false; + _diagnosticsRun = true; + return true; +} + +internal void StashDiagnostics(string message) => _bootstrapDiagnostics = message; + +public string GetBootstrapDiagnostics() => _bootstrapDiagnostics; +``` + +- [ ] **Step 17.2: Add to `IRCommonBuilder`** + +```csharp +string GetBootstrapDiagnostics(); +``` + +- [ ] **Step 17.3: Create the hosted service** + +`Src/RCommon.Core/RCommonBootstrapDiagnosticsHostedService.cs`: + +```csharp +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace RCommon +{ + /// + /// Runs the duplicate-registration scanner once at host startup and emits a single + /// warning (or stashes the message on the builder) if soft duplicates are detected. + /// + internal sealed class RCommonBootstrapDiagnosticsHostedService : IHostedService + { + private readonly IServiceCollection _services; + private readonly IRCommonBuilder _builder; + private readonly ILoggerFactory? _loggerFactory; + + public RCommonBootstrapDiagnosticsHostedService( + IServiceCollection services, + IRCommonBuilder builder, + ILoggerFactory? loggerFactory = null) + { + _services = services; + _builder = builder; + _loggerFactory = loggerFactory; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + if (_builder is not RCommonBuilder rb || !rb.TrySetDiagnosticsRun()) + { + return Task.CompletedTask; + } + + var report = _services.GeneratePossibleDuplicatesServiceDescriptorsString(); + if (string.IsNullOrWhiteSpace(report)) + { + return Task.CompletedTask; + } + + rb.StashDiagnostics(report); + + if (_loggerFactory is not null) + { + var logger = _loggerFactory.CreateLogger(); + logger.LogWarning("RCommon bootstrap detected duplicate service registrations:\n{Report}", report); + } + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; + } +} +``` + +- [ ] **Step 17.4: Register the hosted service from `AddRCommon`** + +In `ServiceCollectionExtensions.AddRCommon`, after creating the new builder (the first-time branch only), add: + +```csharp +services.AddSingleton(sp => + new RCommonBootstrapDiagnosticsHostedService( + services, + sp.GetRequiredService(), + sp.GetService())); +``` + +Note: registering the hosted service inside the first-time branch (after the cache check) ensures it's only registered once even across multiple `AddRCommon` calls. + +- [ ] **Step 17.5: Run new tests, verify pass** + +```bash +dotnet test Tests/RCommon.Core.Tests/RCommon.Core.Tests.csproj --filter "FullyQualifiedName~SoftDuplicateDiagnosticsTests" +``` + +Expected: All 3 tests PASS. + +- [ ] **Step 17.6: Run full Core tests** + +```bash +dotnet test Tests/RCommon.Core.Tests/RCommon.Core.Tests.csproj +``` + +Expected: All tests PASS. + +- [ ] **Step 17.7: Commit** + +```bash +git add Src/RCommon.Core/RCommonBootstrapDiagnosticsHostedService.cs Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs Src/RCommon.Core/RCommonBuilder.cs Src/RCommon.Core/IRCommonBuilder.cs Tests/RCommon.Core.Tests/Bootstrapping/SoftDuplicateDiagnosticsTests.cs +git commit -m "feat(bootstrapping): finalize hosted service emits warning on soft duplicates" +``` + +--- + +## Task 18: Multi-module EF Core integration test + +**Files:** +- Create: `Tests/RCommon.EfCore.Tests/Bootstrapping/MultiModuleEFCoreTests.cs` + +- [ ] **Step 18.1: Read existing EFCore test fixtures to understand DbContext setup** + +```bash +ls Tests/RCommon.EfCore.Tests +``` + +Identify an in-memory DbContext fixture (e.g., something using `UseInMemoryDatabase` or SQLite in-memory). Reuse it if available, otherwise add a minimal test DbContext to the new test file. + +- [ ] **Step 18.2: Create test file** + +```csharp +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using RCommon.Persistence; +using RCommon.Persistence.EFCore; +using Xunit; + +namespace RCommon.EfCore.Tests.Bootstrapping; + +public class MultiModuleEFCoreTests +{ + [Fact] + public void TwoModules_DistinctDbContextsDistinctNames_BothResolvable() + { + var services = new ServiceCollection(); + services.AddLogging(); + + // Module A + services.AddRCommon().WithPersistence(ef => + ef.AddDbContext("DbA", o => o.UseInMemoryDatabase("DbA-" + Guid.NewGuid()))); + + // Module B + services.AddRCommon().WithPersistence(ef => + ef.AddDbContext("DbB", o => o.UseInMemoryDatabase("DbB-" + Guid.NewGuid()))); + + var provider = services.BuildServiceProvider(); + using var scope = provider.CreateScope(); + var factory = scope.ServiceProvider.GetRequiredService(); + factory.Resolve("DbA").Should().BeOfType(); + factory.Resolve("DbB").Should().BeOfType(); + } + + [Fact] + public void TwoModules_SameNameSameContext_IsIdempotent() + { + var services = new ServiceCollection(); + services.AddLogging(); + + services.AddRCommon().WithPersistence(ef => + ef.AddDbContext("DbA", o => o.UseInMemoryDatabase("X"))); + Action secondModule = () => services.AddRCommon().WithPersistence(ef => + ef.AddDbContext("DbA", o => o.UseInMemoryDatabase("X"))); + + secondModule.Should().NotThrow(); + } + + [Fact] + public void TwoModules_SameNameDifferentContext_Throws() + { + var services = new ServiceCollection(); + services.AddLogging(); + + services.AddRCommon().WithPersistence(ef => + ef.AddDbContext("DbA", o => o.UseInMemoryDatabase("X"))); + Action secondModule = () => services.AddRCommon().WithPersistence(ef => + ef.AddDbContext("DbA", o => o.UseInMemoryDatabase("Y"))); + + secondModule.Should().Throw(); + } + + [Fact] + public void TwoModules_RepositoryDescriptors_NotDuplicated() + { + var services = new ServiceCollection(); + services.AddLogging(); + + services.AddRCommon().WithPersistence(ef => + ef.AddDbContext("DbA", o => o.UseInMemoryDatabase("X"))); + services.AddRCommon().WithPersistence(ef => + ef.AddDbContext("DbB", o => o.UseInMemoryDatabase("Y"))); + + // EFCorePerisistenceBuilder registers IReadOnlyRepository<>, IWriteOnlyRepository<>, etc. in its ctor. + // With caching, the ctor runs once, so each descriptor should appear exactly once. + services.Count(d => + d.ServiceType.IsGenericType && + d.ServiceType.GetGenericTypeDefinition() == typeof(IReadOnlyRepository<>)) + .Should().Be(1); + } + + public class TestDbContextA : RCommonDbContext + { + public TestDbContextA(DbContextOptions options) : base(options) { } + } + + public class TestDbContextB : RCommonDbContext + { + public TestDbContextB(DbContextOptions options) : base(options) { } + } +} +``` + +> Verify the exact `IDataStoreFactory.Resolve` signature, `RCommonDbContext` constructor signature, and `IReadOnlyRepository<>` namespace by reading the source. Adjust the test accordingly. + +- [ ] **Step 18.3: Run, verify all pass** + +```bash +dotnet test Tests/RCommon.EfCore.Tests/RCommon.EfCore.Tests.csproj --filter "FullyQualifiedName~MultiModuleEFCoreTests" +``` + +Expected: All 4 tests PASS. + +- [ ] **Step 18.4: Commit** + +```bash +git add Tests/RCommon.EfCore.Tests/Bootstrapping/MultiModuleEFCoreTests.cs +git commit -m "test(efcore): multi-module integration covering merge, idempotent, and conflict" +``` + +--- + +## Task 19: Multi-module MediatR integration test + +**Files:** +- Create: `Tests/RCommon.Mediatr.Tests/Bootstrapping/MultiModuleMediatRTests.cs` + +- [ ] **Step 19.1: Create test file** + +Skeleton (adjust types based on actual MediatR builder API in `Src/RCommon.Mediatr/`): + +```csharp +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using RCommon.EventHandling.Producers; +using RCommon.Models.Events; +using Xunit; + +namespace RCommon.Mediatr.Tests.Bootstrapping; + +public class MultiModuleMediatRTests +{ + [Fact] + public void TwoModules_DistinctProducers_BothRegister() + { + var services = new ServiceCollection(); + + services.AddRCommon().WithEventHandling(eh => + eh.AddProducer()); + services.AddRCommon().WithEventHandling(eh => + eh.AddProducer()); + + services.Count(d => d.ServiceType == typeof(IEventProducer)).Should().Be(2); + } + + [Fact] + public void TwoModules_SameProducer_RegistersOnce() + { + var services = new ServiceCollection(); + + services.AddRCommon().WithEventHandling(eh => + eh.AddProducer()); + services.AddRCommon().WithEventHandling(eh => + eh.AddProducer()); + + services.Count(d => + d.ServiceType == typeof(IEventProducer) && d.ImplementationType == typeof(TestProducerA)) + .Should().Be(1); + } + + public class TestProducerA : IEventProducer + { + public Task ProduceEventAsync(TEvent @event, CancellationToken cancellationToken = default) + where TEvent : ISerializableEvent => Task.CompletedTask; + } + + public class TestProducerB : IEventProducer + { + public Task ProduceEventAsync(TEvent @event, CancellationToken cancellationToken = default) + where TEvent : ISerializableEvent => Task.CompletedTask; + } +} +``` + +- [ ] **Step 19.2: Run, verify pass** + +```bash +dotnet test Tests/RCommon.Mediatr.Tests/RCommon.Mediatr.Tests.csproj --filter "FullyQualifiedName~MultiModuleMediatRTests" +``` + +Expected: Both tests PASS. + +- [ ] **Step 19.3: Commit** + +```bash +git add Tests/RCommon.Mediatr.Tests/Bootstrapping/MultiModuleMediatRTests.cs +git commit -m "test(mediatr): multi-module integration for event producers" +``` + +--- + +## Task 20: Final full-suite verification + +- [ ] **Step 20.1: Build the whole solution** + +```bash +dotnet build Src/RCommon.sln +``` + +Expected: BUILD SUCCESS with no new errors or warnings. + +- [ ] **Step 20.2: Run every test project** + +```bash +dotnet test Src/RCommon.sln +``` + +Expected: All tests PASS. Investigate any failure. + +- [ ] **Step 20.3: Rebase / squash interim commits if desired** + +Per CLAUDE.md: "Before finishing a feature/session, rebase and squash interim commits into a single meaningful commit summarizing the changes." This is at the user's discretion; do not squash without confirming. + +- [ ] **Step 20.4: Open a PR** + +```bash +gh pr create --title "feat(bootstrapping): modular composition of AddRCommon across in-process modules" --body "$(cat <<'EOF' +## Summary + +Makes `services.AddRCommon()` and its fluent verbs composable across multiple modules in a single in-process .NET application without breaking existing single-call usage. Modules can each call `services.AddRCommon()....WithX(...)` and produce a coherent, deduplicated registration set. + +Implements spec [`docs/specs/bootstrapping/bootstrapping.md`](docs/specs/bootstrapping/bootstrapping.md) per design [`docs/superpowers/specs/2026-05-15-modular-bootstrapper-design.md`](docs/superpowers/specs/2026-05-15-modular-bootstrapper-design.md). + +Key changes: +- `IRCommonBuilder` cached as a `ServiceDescriptor.ImplementationInstance` on the `IServiceCollection`; repeated `AddRCommon()` returns the same instance. +- New `GetOrAddBuilder(Func)` helper on `IRCommonBuilder` for sub-builder caching; all `WithX` extensions across the codebase route through it. +- Singleton-style verbs (`WithSimpleGuidGenerator`, `WithSequentialGuidGenerator`, `WithDateTimeSystem`, `WithJsonSerialization`): same impl re-registered is idempotent, different impl throws `RCommonBuilderException` with both type names. +- `DataStoreFactoryOptions.Register<,>`: same `(name, base, concrete)` is idempotent, different concrete under same name throws `UnsupportedDataStoreException` (exception type preserved). +- `AddProducer`: descriptor-scan dedup so identical producer types register once; distinct producer types still coexist. +- New `IHostedService` runs the existing duplicate-descriptor scanner at host startup and emits a single warning on soft duplicates. +- Strictly additive public API: `IRCommonBuilder.GetOrAddBuilder`, `IRCommonBuilder.GetBootstrapDiagnostics()`, `ServiceCollectionExtensions.IsRCommonInitialized()`. + +## Test plan + +- [ ] All existing tests pass +- [ ] New `AddRCommonIdempotencyTests` cover top-level cache behavior +- [ ] New `SubBuilderCacheTests` cover `GetOrAddBuilder` semantics +- [ ] New `SingletonVerbConflictTests` cover singleton-verb idempotency / conflict +- [ ] New `EventProducerDedupTests` cover `AddProducer` dedup +- [ ] New `JsonSerializationSingletonTests` cover `WithJsonSerialization` singleton behavior +- [ ] New `DataStoreFactoryOptionsRegisterTests` cover datastore-name collision +- [ ] New `SoftDuplicateDiagnosticsTests` cover hosted-service warning emission +- [ ] New `MultiModuleEFCoreTests` cover end-to-end EF Core multi-module integration +- [ ] New `MultiModuleMediatRTests` cover end-to-end MediatR multi-module integration +EOF +)" +``` + +(Only do this step when the user explicitly authorizes opening a PR.) + +--- + +## Out of Scope + +- Performance tuning of `GetOrAddBuilder` lookup. +- Concurrent bootstrap (explicitly out of contract per spec). +- An `IRCommonModule` abstraction. +- Migration of third-party `WithX` extensions — they continue to work uncached until they opt in. +- Documentation README updates beyond what's currently in the spec — those are tracked as "Nice to Have" and can be a follow-up PR. + +## Open Items for the Engineer to Resolve at Implementation Time + +- **`DataStoreValue.ConcreteType` property** — confirmed at `Src/RCommon.Persistence/DataStoreValue.cs:50`; Task 8 uses this name. +- **Exact `IEventProducer` method signature** — confirmed by reading `Src/RCommon.Core/EventHandling/Producers/IEventProducer.cs` before Task 9. +- **Wolverine `WithEventHandling` already routed via core** — no separate provider migration task needed for Wolverine. +- **`EventSubscriptionManager.HasProducerForBuilder` may already exist** under another name — read the file before Task 10 step 10.2 and reuse if possible. diff --git a/docs/specs/bootstrapping/bootstrapping.md b/docs/specs/bootstrapping/bootstrapping.md new file mode 100644 index 00000000..e136d942 --- /dev/null +++ b/docs/specs/bootstrapping/bootstrapping.md @@ -0,0 +1,125 @@ +# Bootstrapping + +## Overview + +The bootstrapping domain governs how RCommon's `IServiceCollection` extension API (`AddRCommon()` and its fluent verbs) composes across multiple independent modules in a single in-process .NET application. The contract guarantees that any combination of modules can each invoke `services.AddRCommon()....WithX(...)` and produce a coherent, deduplicated registration set without breaking, throwing on benign repetition, or silently corrupting shared state. + +## Personas + +- **Library consumer (single-module app)** — Calls `services.AddRCommon()` once in `Program.cs`. Expects unchanged behavior from prior RCommon versions. +- **Library consumer (modular app)** — Composes multiple modules where each module's `AddServices(IServiceCollection)` method calls `services.AddRCommon()....WithX(...)`. Expects modules to be order-independent and to fail loudly on genuine misconfiguration, silently on benign repetition. +- **Third-party builder author** — Writes a `WithX` extension method that registers a custom sub-builder. Expects a public, documented helper for participating in the caching contract. +- **RCommon contributor** — Maintains the `AddRCommon()` surface and provider packages. Expects clear conflict semantics per verb type so behavior changes don't silently regress. + +## Core Requirements + +### Must Have + +- `services.AddRCommon()` invoked N times against the same `IServiceCollection` returns the same `IRCommonBuilder` instance (reference equality). Core registrations (`EventSubscriptionManager`, `IEventBus`, `IEventRouter`, `CachingOptions`) fire exactly once. +- A sub-builder verb (`WithPersistence`, `WithMediator`, `WithEventHandling`, `WithMemoryCaching`, `WithDistributedCaching`, `WithJsonSerialization`, `WithCQRS`, `WithValidation`, `WithBlobStorage`, `WithMultiTenancy`, `WithUnitOfWork`) invoked twice with the same concrete `T` causes the sub-builder's constructor to run exactly once. The user's `Action` configuration delegate runs once per call, against the shared cached instance, so configuration accumulates. +- Two sub-builder verbs of the same verb name but **different** concrete `T` register both sub-builders independently. Each has its own cache slot. +- Singleton-style verbs (`WithSimpleGuidGenerator`, `WithSequentialGuidGenerator`, `WithDateTimeSystem`) called twice with the **same** implementation type are idempotent. Called with a **different** implementation type they throw `RCommonBuilderException` naming both types and offering a remediation hint. +- `WithJsonSerialization` follows singleton-style rules across all six overloads — one shared cache slot per `IRCommonBuilder`. +- `IEFCorePersistenceBuilder.AddDbContext(name, options)` invoked twice with the same `(name, TDbContext)` triple is idempotent. Invoked with the same `name` but different `TDbContext` throws `UnsupportedDataStoreException` (existing exception type preserved). +- `AddProducer` with the same concrete `T` is idempotent — at most one `IEventProducer` descriptor exists per concrete producer type. `AddProducer` with distinct concrete types each register and coexist. +- Soft duplicates in the final `IServiceCollection` (any duplicate `ServiceDescriptor` not policed by the rules above) surface as a single `LogLevel.Warning` at host startup via the existing duplicate-scanner machinery. If no `ILoggerFactory` is available, the warning text is retrievable via `IRCommonBuilder.GetBootstrapDiagnostics()`. +- `services.IsRCommonInitialized()` is publicly available and returns `true` if and only if `AddRCommon()` has been invoked against that collection. +- `IRCommonBuilder.GetOrAddBuilder(Func factory)` is publicly available and is the canonical way for `WithX` extensions (including third-party) to participate in the caching contract. + +### Must Not Do + +- Must not change any existing public method or interface signature. +- Must not change any existing exception type thrown by an existing API. +- Must not introduce concurrency-safety guarantees. Bootstrap is single-threaded by contract; modules that initialize in parallel are responsible for synchronizing externally. +- Must not introduce a separate `IRCommonModule` abstraction or any new contract that modules are required to implement. Modules continue to call `services.AddRCommon()` directly. +- Must not require third-party sub-builders to migrate. Pre-existing `WithX` extensions using `Activator.CreateInstance` continue to compile and run; they just don't benefit from the cache until they opt in to `GetOrAddBuilder`. +- Must not silently swallow registration conflicts that indicate genuine module disagreement. Hard conflicts (singleton-type mismatch, datastore-name conflict) throw at the offending call site. + +### Nice to Have + +- A migration documentation snippet in the JsonNet / EfCore / Mediatr README files showing the modular-composition pattern. + +## Technical Constraints + +- .NET 8, .NET 9, .NET 10 targets (inherited from RCommon's existing target frameworks). +- `Microsoft.Extensions.DependencyInjection` API surface (`IServiceCollection`, `ServiceDescriptor`, `TryAdd*` helpers). No third-party DI container. +- `Microsoft.Extensions.Hosting.IHostedService` for the finalize hook. +- `Microsoft.Extensions.Logging.ILoggerFactory` for diagnostic warning emission. +- No new package dependencies introduced. +- TDD per CLAUDE.md — tests precede implementation; red-green-refactor with commits per cycle. *[from: user CLAUDE.md]* + +## Resilience + +No external dependencies. Bootstrap is a one-time in-process configuration step against `IServiceCollection`. No network calls, no I/O, no remote services. **No resilience strategy required.** + +The closest equivalent failure mode — a module raising an exception during its `Action` callback — surfaces as an unhandled exception at startup, which is the correct behavior. RCommon does not catch, suppress, or retry user configuration errors. + +## Observability + +- **Logged:** Soft-duplicate registrations detected at host startup. Emitted once per `IRCommonBuilder` instance as a single `LogLevel.Warning` through `ILoggerFactory.CreateLogger()`. +- **Not logged at this layer:** Per-verb registration calls (too noisy for a library bootstrap); successful idempotent reuses (zero signal value); the conflict-throw cases (already surface as exceptions with full type info in the message). +- **Metrics:** None. Bootstrap is one-shot at startup; there is no steady-state behavior to measure. +- **Alerting:** None at the library layer. Hosts can wire up their own log-based alerts on the warning if desired. +- **Retrieval API:** `IRCommonBuilder.GetBootstrapDiagnostics()` returns the stashed warning text for callers who want programmatic access (typically tests or hosts without an `ILoggerFactory`). + +## Security + +- **Attack surface:** None. This is .NET DI configuration plumbing invoked at process startup by trusted application code. No user input crosses the bootstrapping boundary. +- **Data protection:** None handled. +- **Auth/authz:** N/A. +- **Compliance:** N/A. + +Security review at this layer is limited to ensuring the `RCommonBuilderException` and `UnsupportedDataStoreException` messages do not leak sensitive type names or configuration secrets — both are constrained to type-name reporting, which is non-sensitive. + +## Performance & Scalability + +- **Targets:** Bootstrap completes in O(N) where N is the number of `WithX` calls. Cache lookup is O(1) via `Dictionary`. No performance budget beyond "negligible relative to host startup time." +- **Testing:** None at the performance layer. Functional tests cover the bootstrap path; provider tests cover the resolved-service behavior. There is no load-test scenario applicable to startup configuration. +- **Scaling strategy:** Not applicable. Bootstrap runs once per process. +- **Growth projections:** Not applicable. + +## API Design + +New public surface, strictly additive: + +```csharp +namespace RCommon; + +public interface IRCommonBuilder +{ + // existing members preserved unchanged + TSubBuilder GetOrAddBuilder(Func factory) + where TSubBuilder : class; + string GetBootstrapDiagnostics(); +} + +public static class ServiceCollectionExtensions +{ + // existing members preserved unchanged + public static bool IsRCommonInitialized(this IServiceCollection services); +} +``` + +No removals, no signature changes, no exception-type changes on existing throws. + +## Testing Strategy + +- New test files added under `Tests/RCommon.Tests/Bootstrapping/`, `Tests/RCommon.EfCore.Tests/Bootstrapping/`, `Tests/RCommon.Mediatr.Tests/Bootstrapping/`. +- Coverage anchors: idempotent `AddRCommon`, sub-builder cache invariants, singleton-verb conflict detection, soft-duplicate warning emission, multi-module EF Core integration (including datastore-name collisions), multi-module MediatR integration. +- TDD ordering: core idempotency → cache helper → singleton tracker → datastore-name collision → soft-duplicate finalize → per-provider migrations. +- Existing tests for `_guidConfigured` / `_dateTimeConfigured` / `DataStoreFactoryOptions.Register` need replacement to reflect the relaxed-on-same-type semantics. + +## Migration / Rollout + +- Strictly source-compatible. No release-note "action required" item for consumers using the single-call pattern. +- Modular consumers benefit automatically with no code change. +- Third-party `WithX` extension authors can opt into the cache by switching `Activator.CreateInstance` invocations to `IRCommonBuilder.GetOrAddBuilder` (mechanical edit; documented). +- The existing `[Obsolete]` overloads (`WithPersistence`) receive the same `GetOrAddBuilder` routing so legacy users don't regress. + +## Open Questions + +None at spec-level. All implementation-level open items (exact hosted-service class name, exact filename enumeration for `Src/RCommon.Wolverine/`) are tracked in the source design doc and will be resolved during planning. + +## Feature Breakdown + +This domain is covered by a single specification (no further feature decomposition needed). Detailed implementation specifics — call-site-by-call-site changes, internal data structures, finalize hosted service mechanics — are documented in the brainstorming design at [`docs/superpowers/specs/2026-05-15-modular-bootstrapper-design.md`](../../superpowers/specs/2026-05-15-modular-bootstrapper-design.md), which serves as input to the implementation plan. diff --git a/docs/superpowers/specs/2026-05-15-modular-bootstrapper-design.md b/docs/superpowers/specs/2026-05-15-modular-bootstrapper-design.md new file mode 100644 index 00000000..c1a9d5c2 --- /dev/null +++ b/docs/superpowers/specs/2026-05-15-modular-bootstrapper-design.md @@ -0,0 +1,323 @@ +# Modular Bootstrapper: Composable `AddRCommon` Across Modules + +**Date:** 2026-05-15 +**Status:** Draft +**Breaking Change:** No (strictly additive) + +## Problem + +`services.AddRCommon()` and its fluent verbs are designed for a single composition root. In modular host applications where multiple independent modules each want to register their own RCommon configuration (`Module A` configures EF Core + MediatR, `Module B` configures additional DbContexts + a different event producer), the current bootstrap produces broken or duplicated registrations: + +- `AddRCommon()` creates a fresh `RCommonBuilder` per call. A second call re-registers `EventSubscriptionManager`, `IEventBus`, and `IEventRouter` as duplicate singletons, leaving the second registration's instance unused but resident. +- Instance flags `_guidConfigured` / `_dateTimeConfigured` on `RCommonBuilder` only guard within a single builder. Two `AddRCommon()` calls produce two builders, each with independent flags; the throw-on-second-call protection silently disappears. +- `WithPersistence` and similar verbs `Activator.CreateInstance` a fresh sub-builder on every call. The sub-builder's constructor side-effects (e.g., `services.AddTransient(typeof(IReadOnlyRepository<>), typeof(EFCoreRepository<>))` in [`EFCorePerisistenceBuilder`](../../Src/RCommon.EfCore/EFCorePerisistenceBuilder.cs)) fire repeatedly, producing duplicate transient/scoped descriptors. +- Datastore-name registration via `DataStoreFactoryOptions.Register<,>` accumulates silently. Two modules registering the same name with different `TDbContext` types corrupt the registry without warning. + +The existing [`GeneratePossibleDuplicatesServiceDescriptorsString`](../../Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs#L86-L123) diagnostic acknowledges the problem but is opt-in and runs after the fact. + +## Goal + +Make RCommon bootstrap **composable across modules** in a single process, such that multiple modules can each call `services.AddRCommon()...` and produce a coherent, deduplicated registration set — without changing any public API signatures or breaking existing single-call usage. + +## Decisions + +| Decision | Choice | Rationale | +|---|---|---| +| Cross-call state mechanism | Cache `IRCommonBuilder` as a `ServiceDescriptor.ImplementationInstance` on the `IServiceCollection` | Idiomatic .NET pattern (MassTransit, MediatR, ASP.NET Identity); zero hidden static state; locally inspectable | +| Sub-builder cache key | Concrete sub-builder type (`typeof(EFCorePerisistenceBuilder)`) | Matches existing generic constraint shape; allows valid coexistence of alternative implementations; deterministic collision rule | +| Same-provider, same verb (e.g., `WithPersistence` ×2) | Merge — sub-builder cached, constructor runs once, each call's `Action` runs against the cached instance | Module composition requires accumulation of configuration | +| Different-provider, same verb (e.g., `WithPersistence` + `WithPersistence`) | Allow both — each sub-builder cached independently | Mixed-provider scenarios are already a supported real-world pattern | +| Same datastore name, different concrete `TDbContext` | Throw `UnsupportedDataStoreException` from `DataStoreFactoryOptions.Register<,>` | Domain-level invariant: a datastore name must resolve to exactly one DbContext. Preserves the existing exception type so caller `catch` blocks aren't broken | +| Same datastore name, same `TDbContext` (modular re-registration) | Idempotent no-op | Relaxes today's unconditional throw on duplicate name+base — required for modular composition | +| Singleton-style verbs (`WithSimpleGuidGenerator`, `WithSequentialGuidGenerator`, `WithDateTimeSystem`, `WithJsonSerialization`), same impl re-registered | Idempotent no-op | Realistic modular scenario; matches Options pattern's natural behavior for delegate accumulation | +| Singleton-style verbs, different impl | Throw `RCommonBuilderException` | Today's flag-based protection preserved; ambiguous which module's choice should win | +| `AddProducer` with same producer type repeated | Idempotent — single `IEventProducer` descriptor for that concrete `T`. Implemented by scanning `services` for an existing `ServiceDescriptor` where `ServiceType == typeof(IEventProducer) && ImplementationType == typeof(T)` (or equivalent factory inspection) before adding — **not** `TryAddSingleton`, which would also block subsequent distinct producer types | No real scenario benefits from N instances of the same producer | +| `AddProducer` with different producer types | Coexist | Already the intended design | +| Soft-duplicate detection | Eager hard-throws + auto-warning at finalize via existing duplicate scanner | Hard conflicts fail at the call site; soft duplicates surface visibly but non-fatally | +| Thread-safety contract | Bootstrap is single-threaded; documented, not enforced | Matches .NET-wide assumption about `IServiceCollection` | + +## Design + +### Architecture + +The bootstrap pipeline gains one concept: **shared bootstrap state**, scoped to one `IServiceCollection`, materialized as the cached `IRCommonBuilder` instance. + +``` +IServiceCollection + │ + └─[ServiceDescriptor.Singleton(IRCommonBuilder, instance)]──► IRCommonBuilder + ├─ Services (the IServiceCollection) + ├─ subBuilderCache : Dictionary + ├─ guidRegistration : SingletonRegistration + ├─ dateTimeRegistration : SingletonRegistration + ├─ serializerRegistration : SingletonRegistration + └─ bootstrapDiagnostics : RCommonBootstrapDiagnostics +``` + +**`AddRCommon()` flow:** +1. Look for a `ServiceDescriptor` with `ServiceType == typeof(IRCommonBuilder)` in `services`. +2. Found → return `(IRCommonBuilder)descriptor.ImplementationInstance`. **Return immediately — no further registrations run.** +3. Not found → construct `RCommonBuilder`, register it via `services.AddSingleton(builder)`, then run the one-time core registrations (`EventSubscriptionManager`, `IEventBus`, `IEventRouter`, `CachingOptions`). + +The cache-lookup-and-early-return in step 2 is the **primary** dedup mechanism. The core registrations in step 3 keep their current call shape (`AddSingleton`, `AddScoped`) — they are unreachable on the second call because step 2 already returned. No `TryAdd*` defensive layer is needed here, and adding one would be misleading (it would suggest the path is reachable when it isn't). + +**`WithX(...)` flow** (where `WithX` is `WithPersistence`, `WithUnitOfWork`, `WithMediator`, `WithEventHandling`, `WithMemoryCaching`, `WithDistributedCaching`, `WithJsonSerialization`, `WithCQRS`, `WithValidation`, `WithBlobStorage`, `WithMultiTenancy`): +1. Call `builder.GetOrAddBuilder(() => new TSubBuilder(/* builder.Services or builder, per the sub-builder's ctor */))`. +2. First call for `TSubBuilder`: helper invokes the factory (sub-builder's constructor runs and registers its services exactly once), caches under `typeof(TSubBuilder)`. +3. Subsequent calls for same `TSubBuilder`: helper returns the cached instance without re-invoking the factory. +4. Caller runs the user-supplied `Action` against the returned instance — accumulation is automatic. + +The factory is parameterless because sub-builder constructors in the existing codebase take heterogeneous arguments: persistence sub-builders take `IServiceCollection`, event-handling and JSON sub-builders take `IRCommonBuilder`. The caller closes over whichever it needs. + +**Singleton-style verb flow** (e.g., `WithSimpleGuidGenerator`): +1. Inspect builder's `guidRegistration` — a small mutable struct with two fields: `bool Configured` and `Type? ImplType`. +2. Not configured → register the implementation, set `Configured = true`, `ImplType = typeof(SimpleGuidGenerator)`. +3. Configured with same `ImplType` → no-op (the Options pattern naturally accumulates `Action` delegates if one is supplied). +4. Configured with different `ImplType` → throw `RCommonBuilderException` with both type names and remediation hint. + +(Using a mutable struct rather than a `record` because the field gets reassigned in place during the verb call; `record` semantics would require constant reconstruction.) + +**Finalize flow:** + +The finalize step has a single source of truth: an internal `IHostedService` registered automatically during the first `AddRCommon()` call. It runs once on host startup (after the full DI container is built and all modules have completed their `With*` calls), invokes the scanner once, emits the warning or stashes the message, and then sets a `_diagnosticsRun` flag on the builder so re-entrance is a no-op. + +`IRCommonBuilder.Configure()` (the existing method that returns `IServiceCollection`) does **not** trigger the scanner. It continues to return `Services` unchanged, preserving today's semantics for callers that invoke it explicitly mid-pipeline. + +1. Hosted service `StartAsync` fires. +2. If `_diagnosticsRun` is `true`, return. Otherwise set it to `true`. +3. Run `GeneratePossibleDuplicatesServiceDescriptorsString`. +4. Non-empty result + `ILoggerFactory` resolvable → log as `Warning`. +5. Non-empty result + no logger → stash on `bootstrapDiagnostics` for retrieval via `builder.GetBootstrapDiagnostics()`. + +### New public surface + +```csharp +namespace RCommon; + +public interface IRCommonBuilder +{ + // existing members preserved unchanged + + /// + /// Returns the cached sub-builder for if one exists, + /// otherwise invokes , caches the result, and returns it. + /// + /// Concrete sub-builder type (e.g., EFCorePerisistenceBuilder). + /// Parameterless factory invoked exactly once per per . Callers close over whatever constructor argument they need — typically builder.Services or builder itself, depending on the sub-builder's constructor. + TSubBuilder GetOrAddBuilder(Func factory) + where TSubBuilder : class; + + /// + /// Returns soft-duplicate diagnostic output stashed at finalize when no + /// was available. Empty string if no duplicates were detected. + /// + string GetBootstrapDiagnostics(); +} + +public static class ServiceCollectionExtensions +{ + // existing members preserved unchanged + + /// + /// Returns true if has been invoked against this collection. + /// + public static bool IsRCommonInitialized(this IServiceCollection services); +} +``` + +No removals, no signature changes. + +### Modified call sites + +| File | Change | +|---|---| +| [`Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs`](../../Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs) | `AddRCommon()` looks up the cached `IRCommonBuilder` descriptor; returns cached if present, otherwise constructs and registers as singleton instance. Registers the finalize `IHostedService`. Adds `IsRCommonInitialized()`. | +| [`Src/RCommon.Core/RCommonBuilder.cs`](../../Src/RCommon.Core/RCommonBuilder.cs) | Bare `_guidConfigured` (current text: `"Guid Generator has already been configured once. You cannot configure multiple times"`) and `_dateTimeConfigured` (current text: `"Date/Time System has already been configured once. You cannot configure multiple times"`) replaced with `SingletonRegistration` structs tracking impl type. Verbs relaxed to idempotent on same-type, throw on different-type. Implements `GetOrAddBuilder` and `GetBootstrapDiagnostics()`. | +| [`Src/RCommon.Core/EventHandling/EventHandlingBuilderExtensions.cs`](../../Src/RCommon.Core/EventHandling/EventHandlingBuilderExtensions.cs) | `WithEventHandling` routes through `GetOrAddBuilder`. `AddProducer` (all three overloads) scans `services` for an existing `(IEventProducer, T)` descriptor and skips if present; **does not** use `TryAddSingleton` (which would also block distinct producer types). `EventSubscriptionManager.AddProducerForBuilder` becomes set-based to ignore repeated identical associations. | +| [`Src/RCommon.Persistence/PersistenceBuilderExtensions.cs`](../../Src/RCommon.Persistence/PersistenceBuilderExtensions.cs) | `WithPersistence` and `WithUnitOfWork` route through `GetOrAddBuilder`. `WithEventTracking` switches `AddScoped` → `TryAddScoped`. Deprecated `WithPersistence` overloads receive the same routing. | +| [`Src/RCommon.Persistence/DataStoreFactoryOptions.cs`](../../Src/RCommon.Persistence/DataStoreFactoryOptions.cs) | `Register(string name)` semantics changed: (`name`, `B`, `C`) identical = idempotent no-op; (`name`, `B`) identical but `C` differs = throws `UnsupportedDataStoreException` with both concrete-type names. Exception type **preserved** — does not change to `RCommonBuilderException`. | +| [`Src/RCommon.Mediator/MediatorBuilderExtensions.cs`](../../Src/RCommon.Mediator/MediatorBuilderExtensions.cs) | `WithMediator` (both overloads) routes through `GetOrAddBuilder`. | +| [`Src/RCommon.Mediatr/MediatREventHandlingBuilderExtensions.cs`](../../Src/RCommon.Mediatr/MediatREventHandlingBuilderExtensions.cs) | `WithEventHandling` (all three overloads) routes through `GetOrAddBuilder`. Internal MediatR `services.AddMediatR` accumulates naturally and does not need additional dedup. | +| [`Src/RCommon.MassTransit/MassTransitEventHandlingBuilderExtensions.cs`](../../Src/RCommon.MassTransit/MassTransitEventHandlingBuilderExtensions.cs) | `WithEventHandling` (both overloads) routes through `GetOrAddBuilder`. | +| [`Src/RCommon.Wolverine/`](../../Src/RCommon.Wolverine/) | Any `WithMediator` / `WithEventHandling` extensions in this package route through `GetOrAddBuilder`. (Exact filenames to be enumerated during implementation; pattern is uniform.) | +| [`Src/RCommon.Caching/CachingBuilderExtensions.cs`](../../Src/RCommon.Caching/CachingBuilderExtensions.cs) | `WithMemoryCaching` and `WithDistributedCaching` (both overloads each) route through `GetOrAddBuilder`. | +| [`Src/RCommon.MemoryCache/`](../../Src/RCommon.MemoryCache/) | Sub-builder side of `WithMemoryCaching` — no code change needed if it follows the constructor-side-effect pattern; caching at the parent prevents re-entry. | +| [`Src/RCommon.RedisCache/`](../../Src/RCommon.RedisCache/) | Same. | +| [`Src/RCommon.Json/JsonBuilderExtensions.cs`](../../Src/RCommon.Json/JsonBuilderExtensions.cs) | `WithJsonSerialization` becomes singleton-style across all **six** existing overloads (parameterless, serialize-only, deserialize-only, both, action-only, full). The builder's `jsonRegistration` slot tracks the impl type; same `T` = idempotent (the `Action` is still applied to the cached instance via `GetOrAddBuilder`); different `T` = throw `RCommonBuilderException`. | +| [`Src/RCommon.JsonNet/`](../../Src/RCommon.JsonNet/), [`Src/RCommon.SystemTextJson/`](../../Src/RCommon.SystemTextJson/) | Concrete builder types are `JsonNetBuilder` and `TextJsonBuilder`. No constructor changes needed. | +| [`Src/RCommon.ApplicationServices/CqrsBuilderExtensions.cs`](../../Src/RCommon.ApplicationServices/CqrsBuilderExtensions.cs) | `WithCQRS` (both overloads) routes through `GetOrAddBuilder`. | +| [`Src/RCommon.ApplicationServices/ValidationBuilderExtensions.cs`](../../Src/RCommon.ApplicationServices/ValidationBuilderExtensions.cs), [`Src/RCommon.FluentValidation/ValidationBuilderExtensions.cs`](../../Src/RCommon.FluentValidation/ValidationBuilderExtensions.cs) | `WithValidation` (both overloads each) routes through `GetOrAddBuilder`. | +| [`Src/RCommon.Blobs/BlobStorageBuilderExtensions.cs`](../../Src/RCommon.Blobs/BlobStorageBuilderExtensions.cs) | `WithBlobStorage` (both overloads) routes through `GetOrAddBuilder`. | +| [`Src/RCommon.MultiTenancy/MultiTenancyBuilderExtensions.cs`](../../Src/RCommon.MultiTenancy/MultiTenancyBuilderExtensions.cs) | `WithMultiTenancy` routes through `GetOrAddBuilder`. | +| [`Src/RCommon.Stateless/StatelessBuilderExtensions.cs`](../../Src/RCommon.Stateless/StatelessBuilderExtensions.cs), [`Src/RCommon.MassTransit.StateMachines/`](../../Src/RCommon.MassTransit.StateMachines/) | `WithStatelessStateMachine` / `WithMassTransitStateMachine` underlying registrations made idempotent via `TryAdd*` where they currently use plain `Add*`. These verbs take no generic argument, so no sub-builder cache slot is needed. | +| [`Src/RCommon.Emailing/EmailingBuilderExtensions.cs`](../../Src/RCommon.Emailing/EmailingBuilderExtensions.cs), [`Src/RCommon.SendGrid/SendGridEmailingConfigurationExtensions.cs`](../../Src/RCommon.SendGrid/SendGridEmailingConfigurationExtensions.cs) | `WithSmtpEmailServices` / `WithSendGridEmailServices` underlying registrations made idempotent via `TryAdd*`. Singleton-style: only one `IEmailService` impl makes sense per app; mixing throws via a new `emailRegistration` slot on the builder. | +| [`Src/RCommon.Security/SecurityConfigurationExtensions.cs`](../../Src/RCommon.Security/SecurityConfigurationExtensions.cs), [`Src/RCommon.Web/WebConfigurationExtensions.cs`](../../Src/RCommon.Web/WebConfigurationExtensions.cs) | `WithClaimsAndPrincipalAccessor` / `WithClaimsAndPrincipalAccessorForWeb` registrations switch to `TryAdd*`. Singleton-style in spirit but no generic arg — the impl type is fixed per overload, so `TryAdd*` is sufficient. | + +**Unchanged:** all sub-builder constructors (caching prevents re-entry), all public fluent surface signatures, the `RCommonBuilder` constructor's external behavior, all existing test fixtures for single-call usage. + +### Conflict matrix (authoritative) + +| Verb / scenario | Same impl | Different impl | Notes | +|---|---|---|---| +| `AddRCommon()` | Idempotent (returns cached builder) | n/a | Core registrations fire exactly once | +| `WithPersistence` | Merge (cached sub-builder, action accumulates) | Both register | | +| `WithUnitOfWork` | Merge | Both register | | +| `WithMediator` | Merge | Both register | | +| `WithEventHandling` | Merge | Both register | | +| `WithMemoryCaching` / `WithDistributedCaching` | Merge | Both register | | +| `WithJsonSerialization` | Idempotent (action still applied to cached instance) | Throws (different concrete `T`) | Singleton-style. All six overloads share one cache slot | +| `WithCQRS` | Merge | Both register | | +| `WithValidation` | Merge | Both register | | +| `WithBlobStorage` | Merge | Both register | | +| `WithMultiTenancy` | Merge | Both register | | +| `WithSmtpEmailServices` / `WithSendGridEmailServices` | Idempotent (`TryAdd*`) | Throws (different email-impl slot) | Singleton-style | +| `WithStatelessStateMachine` / `WithMassTransitStateMachine` | Idempotent (`TryAdd*`) | n/a (single impl per verb) | | +| `WithClaimsAndPrincipalAccessor` / `WithClaimsAndPrincipalAccessorForWeb` | Idempotent (`TryAdd*`) | n/a (single impl per verb) | | +| `WithSequentialGuidGenerator` / `WithSimpleGuidGenerator` | Idempotent | Throws | Singleton-style | +| `WithDateTimeSystem` | Idempotent (options actions accumulate) | n/a (one impl type) | | +| `WithCommonFactory` | Idempotent (`TryAdd`) | Last-wins (standard DI) + soft-dup warning | Not policed by RCommon | +| `AddProducer` | Idempotent (same `(IEventProducer, T)` descriptor detected and skipped) | Coexist | | +| `IEFCorePersistenceBuilder.AddDbContext(name, ...)` | Idempotent (same name + same `TDbContext`) | Throws `UnsupportedDataStoreException` (same name + different `TDbContext`) | Enforced in `DataStoreFactoryOptions.Register<,>` — exception type unchanged from today | + +### Error message shape + +Every `RCommonBuilderException` thrown by these rules includes: +- The conflicting verb name (e.g., `"WithSimpleGuidGenerator"`) +- The previously-recorded implementation type's full name +- The newly-attempted implementation type's full name +- A short remediation hint: `"To configure multiple modules consistently, ensure all modules agree on the same X implementation, or designate a single composition root that performs this registration."` + +No structured error metadata beyond the message. Stack trace identifies the offending call site. + +### Lifetime & thread safety + +- The cached `IRCommonBuilder` is held by the `IServiceCollection` via its singleton descriptor and transitions into the `ServiceProvider` on `BuildServiceProvider()`. Heap retention: a few kilobytes per provider for the lifetime of the host. +- The builder holds no unmanaged resources; no `IDisposable` contract. +- Bootstrap is **not thread-safe**. Documented contract: all `AddRCommon()` and `With*` invocations must be serialized at the host level. Matches the `IServiceCollection`-wide convention. + +### Diagnostics + +The existing [`GeneratePossibleDuplicatesServiceDescriptorsString`](../../Src/RCommon.Core/Extensions/ServiceCollectionExtensions.cs#L86-L123) scanner is wired to run automatically via the finalize `IHostedService` described in the Finalize flow above. `IRCommonBuilder.Configure()` does **not** trigger the scanner. + +Emission: +- `ILoggerFactory` resolvable → single `LogLevel.Warning` containing the report. +- Not resolvable → message stashed; `builder.GetBootstrapDiagnostics()` returns it. + +The scanner runs at most once per builder instance, guarded by the `_diagnosticsRun` flag. + +## Migration & backward compatibility + +**Source compatibility:** strictly additive. No existing signature changes, no member removals. Existing single-call code recompiles without diff. + +**Behavioral compatibility:** +- Single `AddRCommon()` + single set of `With*` calls: bit-identical final `IServiceCollection` contents (the new `TryAdd` paths have nothing to deduplicate). +- Two `AddRCommon()` calls with no `With*` between: today, two `RCommonBuilder` instances are created but only the second's state is reachable to the caller, and the first's redundant core descriptors sit unused. After the change, both calls return the same builder with a single set of core descriptors. **Net registration outcome is functionally equivalent** for any code that wasn't broken. +- **Three deliberate throw-relaxations** (all are pure relaxations — code that wasn't throwing won't start throwing): + - `_guidConfigured`: today throws `RCommonBuilderException("Guid Generator has already been configured once. You cannot configure multiple times")` on any second call regardless of impl type. After change: same impl type = idempotent, different impl type = throws. + - `_dateTimeConfigured`: today throws `RCommonBuilderException("Date/Time System has already been configured once. You cannot configure multiple times")` on any second call. After change: idempotent (only one impl type exists). + - `DataStoreFactoryOptions.Register(name)`: today throws `UnsupportedDataStoreException` on any duplicate `(name, B)` regardless of `C`. After change: same `(name, B, C)` = idempotent, different `(name, B, C')` = throws with the **same exception type**. Caller `catch (UnsupportedDataStoreException)` blocks continue to work. +- No existing exception types change. `RCommonBuilderException` is the type used by all *new* throws (singleton-conflict cases), preserving existing exception contracts. + +**Third-party `WithX` extensions:** continue to compile and run, but won't benefit from the cache until migrated to `GetOrAddBuilder`. Migration is mechanical: + +```csharp +// Before (constructor takes IServiceCollection — e.g., persistence sub-builders) +var sub = (TSub)Activator.CreateInstance(typeof(TSub), new object[] { builder.Services })!; +actions(sub); + +// After +var sub = builder.GetOrAddBuilder(() => (TSub)Activator.CreateInstance(typeof(TSub), new object[] { builder.Services })!); +actions(sub); + +// Before (constructor takes IRCommonBuilder — e.g., event-handling and json sub-builders) +var sub = (TSub)Activator.CreateInstance(typeof(TSub), new object[] { builder })!; +actions(sub); + +// After +var sub = builder.GetOrAddBuilder(() => (TSub)Activator.CreateInstance(typeof(TSub), new object[] { builder })!); +actions(sub); +``` + +**Deprecated overloads:** the `[Obsolete]` `WithPersistence` overloads receive the same `GetOrAddBuilder` routing to prevent legacy users from regressing. + +## Failure modes + +| Failure | Surface | +|---|---| +| Conflicting singleton types across modules | `RCommonBuilderException` thrown synchronously from the second `With*` call | +| Same datastore name with different `TDbContext` | `UnsupportedDataStoreException` from `DataStoreFactoryOptions.Register<,>` (eager — fires at the `AddDbContext` call) | +| Soft duplicates in service descriptors | Single `LogLevel.Warning` at finalize, or stashed in `GetBootstrapDiagnostics()` | +| `null` `IServiceCollection` | Existing `Guard.Against` continues to throw | +| Sub-builder construction failure | Bubbles from `Activator.CreateInstance` / `new TSubBuilder(services)` as today | + +## Testing + +### Layout + +``` +Tests/ + RCommon.Tests/Bootstrapping/ + AddRCommonIdempotencyTests.cs + SubBuilderCacheTests.cs + SingletonVerbConflictTests.cs + SoftDuplicateDiagnosticsTests.cs + RCommon.EfCore.Tests/Bootstrapping/ + MultiModuleEFCoreTests.cs + RCommon.Mediatr.Tests/Bootstrapping/ + MultiModuleMediatRTests.cs +``` + +### Coverage + +**`AddRCommonIdempotencyTests`** +- Two `AddRCommon()` calls return the reference-equal `IRCommonBuilder`. +- After two calls, descriptor count for `EventSubscriptionManager`, `IEventBus`, `IEventRouter`, `CachingOptions` is 1 each. +- After two no-`With*` calls, descriptor count is identical to one call. +- `IsRCommonInitialized()` is `false` before, `true` after. + +**`SubBuilderCacheTests`** +- `WithPersistence` twice → sub-builder constructor invoked once (counter in test-only sub-builder). +- Each call's `Action` runs against the same cached instance. +- `WithPersistence` and `WithPersistence` produce two distinct cached sub-builders. +- `GetOrAddBuilder` invokes the factory exactly once across repeated lookups. + +**`SingletonVerbConflictTests`** +- `WithSimpleGuidGenerator()` twice → no throw, single `IGuidGenerator` descriptor. (Today this throws unconditionally — test must replace the existing throw-assertion fixture.) +- `WithSimpleGuidGenerator()` then `WithSequentialGuidGenerator(...)` → throws `RCommonBuilderException`; message names both types. +- Symmetric: reverse order → throws with equivalent message. +- `WithDateTimeSystem(...)` twice → no throw. (Today this throws unconditionally — replaces existing assertion.) +- `WithJsonSerialization()` then `WithJsonSerialization()` → throws. +- `WithJsonSerialization(serializeOpts1)` then `WithJsonSerialization(serializeOpts2)` → no throw; both options actions applied to the cached builder. + +**`SoftDuplicateDiagnosticsTests`** +- Manually injecting a duplicate `(IFoo, FooImpl)` after `AddRCommon` → at host startup, the finalize `IHostedService` runs and emits one warning through a test `ILoggerFactory`. +- Zero soft duplicates → no warning emitted. +- Warning emitted at most once across repeated host starts within a single test context. +- `Configure()` invocation does **not** trigger the scanner (preserves existing semantics). +- `GetBootstrapDiagnostics()` returns the message when no logger factory is registered. + +**`MultiModuleEFCoreTests`** +- Two simulated modules each call `AddRCommon().WithPersistence` registering distinct DbContexts under distinct names. Built provider resolves `IDataStoreFactory` and obtains both DbContexts. +- Identical `AddDbContext("DbA", ...)` from two modules → no throw, single registration. (Today this throws `UnsupportedDataStoreException` — test replaces the existing throw-assertion.) +- Same name + different `TDbContext` across two modules → throws `UnsupportedDataStoreException` (exception type unchanged). +- Two-module scenario produces exactly 1 descriptor for each repository interface (`IReadOnlyRepository<>`, `IWriteOnlyRepository<>`, etc.), proving constructor side-effects ran once. + +**`MultiModuleMediatRTests`** +- Two modules each call `WithEventHandling` and `AddProducer` / `AddProducer` → both producers register, both receive events. +- Two modules both call `AddProducer` → exactly one descriptor. + +### TDD ordering + +1. `AddRCommonIdempotencyTests` → drives the cached-builder mechanic in `ServiceCollectionExtensions`. +2. `SubBuilderCacheTests` → drives `GetOrAddBuilder` on `IRCommonBuilder`. +3. `SingletonVerbConflictTests` → drives the `SingletonRegistration` tracker. +4. `MultiModuleEFCoreTests` → drives `DataStoreFactoryOptions.Register<,>` collision detection plus full integration. +5. `SoftDuplicateDiagnosticsTests` → drives the finalize-time scanner. +6. Provider migrations come last; each is a small mechanical edit + smoke test. + +## Out of scope + +- Each provider's own functional correctness (already covered by existing test suites). +- Concurrent bootstrap. Explicitly out of contract. +- Performance tuning of `GetOrAddBuilder` lookup (`Dictionary` is O(1); bootstrap path is one-time-at-startup). +- A separate `IRCommonModule` abstraction. The user-facing pattern remains "modules call `services.AddRCommon()`". diff --git a/website/.docusaurus/@easyops-cn/docusaurus-search-local/default/generated.js b/website/.docusaurus/@easyops-cn/docusaurus-search-local/default/generated.js index c9664049..c5dd3507 100644 --- a/website/.docusaurus/@easyops-cn/docusaurus-search-local/default/generated.js +++ b/website/.docusaurus/@easyops-cn/docusaurus-search-local/default/generated.js @@ -3,7 +3,7 @@ export const language = ["en"]; export const removeDefaultStopWordFilter = false; export const removeDefaultStemmer = false; export { default as Mark } from "C:\\Users\\jason.webb\\source\\repos\\RCommon\\website\\node_modules\\.pnpm\\mark.js@8.11.1\\node_modules\\mark.js\\dist\\mark.js" -export const searchIndexUrl = "search-index{dir}.json?_=46f308b2"; +export const searchIndexUrl = "search-index{dir}.json?_=6fe05f60"; export const searchResultLimits = 8; export const searchResultContextMaxLength = 50; export const explicitSearchResultPath = true; diff --git a/website/.docusaurus/client-manifest.json b/website/.docusaurus/client-manifest.json index 10c1765f..c1f891be 100644 --- a/website/.docusaurus/client-manifest.json +++ b/website/.docusaurus/client-manifest.json @@ -863,6 +863,9 @@ 1869, 8764 ], + "e3ba908e": [ + 7134 + ], "e3d4b007": [ 9774 ], @@ -1113,9 +1116,9 @@ "139": { "js": [ { - "file": "assets/js/e0146154.a6aea184.js", - "hash": "d4703b7bf51327bb", - "publicPath": "/assets/js/e0146154.a6aea184.js" + "file": "assets/js/e0146154.2a6d494f.js", + "hash": "0d5a8beaaac734c1", + "publicPath": "/assets/js/e0146154.2a6d494f.js" } ] }, @@ -1167,9 +1170,9 @@ "323": { "js": [ { - "file": "assets/js/523cdb73.8382bb4a.js", - "hash": "45b29b96c8d08131", - "publicPath": "/assets/js/523cdb73.8382bb4a.js" + "file": "assets/js/523cdb73.28deca8e.js", + "hash": "6c2f1c01d4006eb9", + "publicPath": "/assets/js/523cdb73.28deca8e.js" } ] }, @@ -1194,9 +1197,9 @@ "584": { "js": [ { - "file": "assets/js/da584be8.4ce44815.js", - "hash": "94b3fb14a61ac792", - "publicPath": "/assets/js/da584be8.4ce44815.js" + "file": "assets/js/da584be8.793fc1b9.js", + "hash": "d6bd548a20c732c4", + "publicPath": "/assets/js/da584be8.793fc1b9.js" } ] }, @@ -1212,9 +1215,9 @@ "641": { "js": [ { - "file": "assets/js/afa07236.662fea9d.js", - "hash": "4959ddadb7a51a76", - "publicPath": "/assets/js/afa07236.662fea9d.js" + "file": "assets/js/afa07236.96328829.js", + "hash": "f1584d9a47ac3573", + "publicPath": "/assets/js/afa07236.96328829.js" } ] }, @@ -1230,9 +1233,9 @@ "688": { "js": [ { - "file": "assets/js/1900a2f2.50e47a8a.js", - "hash": "94f42eae63c5c182", - "publicPath": "/assets/js/1900a2f2.50e47a8a.js" + "file": "assets/js/1900a2f2.6fdfc7ab.js", + "hash": "4cd5c52a839b89cb", + "publicPath": "/assets/js/1900a2f2.6fdfc7ab.js" } ] }, @@ -1401,9 +1404,9 @@ "1262": { "js": [ { - "file": "assets/js/af358a8e.f9424cac.js", - "hash": "701e2a030cc0a65e", - "publicPath": "/assets/js/af358a8e.f9424cac.js" + "file": "assets/js/af358a8e.9094cab3.js", + "hash": "6cc0ba1434b24c10", + "publicPath": "/assets/js/af358a8e.9094cab3.js" } ] }, @@ -1419,9 +1422,9 @@ "1375": { "js": [ { - "file": "assets/js/72caafa9.a96c59ad.js", - "hash": "c55c2c398bf54950", - "publicPath": "/assets/js/72caafa9.a96c59ad.js" + "file": "assets/js/72caafa9.5c7359ff.js", + "hash": "425f75c11a5c4107", + "publicPath": "/assets/js/72caafa9.5c7359ff.js" } ] }, @@ -1455,9 +1458,9 @@ "1475": { "js": [ { - "file": "assets/js/d7ba9c2b.f41cebea.js", - "hash": "f6ae5c997b450fb6", - "publicPath": "/assets/js/d7ba9c2b.f41cebea.js" + "file": "assets/js/d7ba9c2b.3e849bca.js", + "hash": "4a05be962575336a", + "publicPath": "/assets/js/d7ba9c2b.3e849bca.js" } ] }, @@ -1554,18 +1557,18 @@ "1880": { "js": [ { - "file": "assets/js/7d8ddf48.b39b3c4e.js", - "hash": "1ee64f08288c0b7c", - "publicPath": "/assets/js/7d8ddf48.b39b3c4e.js" + "file": "assets/js/7d8ddf48.613f0a94.js", + "hash": "d27ed1d70e2341e7", + "publicPath": "/assets/js/7d8ddf48.613f0a94.js" } ] }, "1909": { "js": [ { - "file": "assets/js/2fb564d3.a788fc33.js", - "hash": "b0fa38da00f13cde", - "publicPath": "/assets/js/2fb564d3.a788fc33.js" + "file": "assets/js/2fb564d3.fdcbd40a.js", + "hash": "577b2e94818558c4", + "publicPath": "/assets/js/2fb564d3.fdcbd40a.js" } ] }, @@ -1581,9 +1584,9 @@ "2042": { "js": [ { - "file": "assets/js/8a13d96c.e7d1c992.js", - "hash": "810022fbe46f7c82", - "publicPath": "/assets/js/8a13d96c.e7d1c992.js" + "file": "assets/js/8a13d96c.9eb6a0aa.js", + "hash": "ab7b81ca6fa2deeb", + "publicPath": "/assets/js/8a13d96c.9eb6a0aa.js" } ] }, @@ -1617,9 +1620,9 @@ "2165": { "js": [ { - "file": "assets/js/0e171350.a8de79e8.js", - "hash": "1fd9d7bc4934dc3c", - "publicPath": "/assets/js/0e171350.a8de79e8.js" + "file": "assets/js/0e171350.c5b2e15a.js", + "hash": "21fd537d7b5faf98", + "publicPath": "/assets/js/0e171350.c5b2e15a.js" } ] }, @@ -1689,9 +1692,9 @@ "2617": { "js": [ { - "file": "assets/js/195c7c19.17c6dcea.js", - "hash": "f58d056e6508b952", - "publicPath": "/assets/js/195c7c19.17c6dcea.js" + "file": "assets/js/195c7c19.183c646e.js", + "hash": "e5ea36f8848aa7c2", + "publicPath": "/assets/js/195c7c19.183c646e.js" } ] }, @@ -1707,9 +1710,9 @@ "2704": { "js": [ { - "file": "assets/js/364ef54b.04095105.js", - "hash": "61580fb0bf23f7c9", - "publicPath": "/assets/js/364ef54b.04095105.js" + "file": "assets/js/364ef54b.3133a7a2.js", + "hash": "9145b40526ed1055", + "publicPath": "/assets/js/364ef54b.3133a7a2.js" } ] }, @@ -1878,9 +1881,9 @@ "3709": { "js": [ { - "file": "assets/js/8b219ab3.219ee8ba.js", - "hash": "a3cc9f3601523606", - "publicPath": "/assets/js/8b219ab3.219ee8ba.js" + "file": "assets/js/8b219ab3.1bd3a992.js", + "hash": "c68edfc0e4bd3412", + "publicPath": "/assets/js/8b219ab3.1bd3a992.js" } ] }, @@ -1914,9 +1917,9 @@ "4006": { "js": [ { - "file": "assets/js/114ecb34.4bf295b8.js", - "hash": "46fb2215501720a2", - "publicPath": "/assets/js/114ecb34.4bf295b8.js" + "file": "assets/js/114ecb34.2b676dcc.js", + "hash": "74ce611d64abece3", + "publicPath": "/assets/js/114ecb34.2b676dcc.js" } ] }, @@ -1950,18 +1953,18 @@ "4096": { "js": [ { - "file": "assets/js/06c1478b.3598bb04.js", - "hash": "d4134fa22afe4478", - "publicPath": "/assets/js/06c1478b.3598bb04.js" + "file": "assets/js/06c1478b.61376df1.js", + "hash": "74548e68103eb153", + "publicPath": "/assets/js/06c1478b.61376df1.js" } ] }, "4103": { "js": [ { - "file": "assets/js/5e6bb026.70347693.js", - "hash": "fc9cfb31d82261d1", - "publicPath": "/assets/js/5e6bb026.70347693.js" + "file": "assets/js/5e6bb026.24e33121.js", + "hash": "6a49670d5f95bfef", + "publicPath": "/assets/js/5e6bb026.24e33121.js" } ] }, @@ -2013,9 +2016,9 @@ "4444": { "js": [ { - "file": "assets/js/51ddfa24.0b2fd76c.js", - "hash": "c116d9187331499d", - "publicPath": "/assets/js/51ddfa24.0b2fd76c.js" + "file": "assets/js/51ddfa24.bb1f37e9.js", + "hash": "fd429a931a3cb160", + "publicPath": "/assets/js/51ddfa24.bb1f37e9.js" } ] }, @@ -2103,9 +2106,9 @@ "4890": { "js": [ { - "file": "assets/js/30d27832.08d95ec6.js", - "hash": "da41ce51c99901fb", - "publicPath": "/assets/js/30d27832.08d95ec6.js" + "file": "assets/js/30d27832.362bcc3b.js", + "hash": "400b8538132aaabe", + "publicPath": "/assets/js/30d27832.362bcc3b.js" } ] }, @@ -2130,9 +2133,9 @@ "4959": { "js": [ { - "file": "assets/js/b1929690.986a66fe.js", - "hash": "d510342437d4d481", - "publicPath": "/assets/js/b1929690.986a66fe.js" + "file": "assets/js/b1929690.566cba90.js", + "hash": "b8d4e7126a438343", + "publicPath": "/assets/js/b1929690.566cba90.js" } ] }, @@ -2148,18 +2151,18 @@ "4994": { "js": [ { - "file": "assets/js/3df138ae.7d21539c.js", - "hash": "784c4e333604e1ae", - "publicPath": "/assets/js/3df138ae.7d21539c.js" + "file": "assets/js/3df138ae.69c963cb.js", + "hash": "31d01cfa57dad051", + "publicPath": "/assets/js/3df138ae.69c963cb.js" } ] }, "5088": { "js": [ { - "file": "assets/js/706a23ff.6f429037.js", - "hash": "5c4c1831feb0bf41", - "publicPath": "/assets/js/706a23ff.6f429037.js" + "file": "assets/js/706a23ff.c533cbeb.js", + "hash": "04402cd35eba2aff", + "publicPath": "/assets/js/706a23ff.c533cbeb.js" } ] }, @@ -2220,9 +2223,9 @@ "5354": { "js": [ { - "file": "assets/js/runtime~main.c8cb0ce2.js", - "hash": "b4fbc5d82981cb4b", - "publicPath": "/assets/js/runtime~main.c8cb0ce2.js" + "file": "assets/js/runtime~main.b41f9dd5.js", + "hash": "9006b65bec643291", + "publicPath": "/assets/js/runtime~main.b41f9dd5.js" } ] }, @@ -2256,18 +2259,18 @@ "5493": { "js": [ { - "file": "assets/js/5f236a12.a910bf04.js", - "hash": "96e68c877e93d936", - "publicPath": "/assets/js/5f236a12.a910bf04.js" + "file": "assets/js/5f236a12.dfa49e9c.js", + "hash": "4ef1bb7a656dd66b", + "publicPath": "/assets/js/5f236a12.dfa49e9c.js" } ] }, "5531": { "js": [ { - "file": "assets/js/3cd55c21.12c791a5.js", - "hash": "f4f42cb30bc6c84d", - "publicPath": "/assets/js/3cd55c21.12c791a5.js" + "file": "assets/js/3cd55c21.8f2d82e8.js", + "hash": "97a65af8d9cec51d", + "publicPath": "/assets/js/3cd55c21.8f2d82e8.js" } ] }, @@ -2283,9 +2286,9 @@ "5585": { "js": [ { - "file": "assets/js/a283db28.868ee359.js", - "hash": "08fa9420c76bebc2", - "publicPath": "/assets/js/a283db28.868ee359.js" + "file": "assets/js/a283db28.fd3bb2cf.js", + "hash": "d3405455345a5b47", + "publicPath": "/assets/js/a283db28.fd3bb2cf.js" } ] }, @@ -2337,18 +2340,18 @@ "5949": { "js": [ { - "file": "assets/js/6d12448e.4f619896.js", - "hash": "25749ab56c74a07f", - "publicPath": "/assets/js/6d12448e.4f619896.js" + "file": "assets/js/6d12448e.9b29942f.js", + "hash": "368a7afcfc7e00c7", + "publicPath": "/assets/js/6d12448e.9b29942f.js" } ] }, "5999": { "js": [ { - "file": "assets/js/462845a6.04e309d6.js", - "hash": "6f796816a0ffa017", - "publicPath": "/assets/js/462845a6.04e309d6.js" + "file": "assets/js/462845a6.7c1ea3fd.js", + "hash": "c30146ae7f37d180", + "publicPath": "/assets/js/462845a6.7c1ea3fd.js" } ] }, @@ -2409,18 +2412,18 @@ "6488": { "js": [ { - "file": "assets/js/58715ff3.da7f2898.js", - "hash": "79f0b81b7e7c72f7", - "publicPath": "/assets/js/58715ff3.da7f2898.js" + "file": "assets/js/58715ff3.e94761a2.js", + "hash": "78831404b19096b5", + "publicPath": "/assets/js/58715ff3.e94761a2.js" } ] }, "6581": { "js": [ { - "file": "assets/js/5a4fc6f7.bdcce958.js", - "hash": "da5d5654d5d1e510", - "publicPath": "/assets/js/5a4fc6f7.bdcce958.js" + "file": "assets/js/5a4fc6f7.0fb85829.js", + "hash": "1d873cd1de31141c", + "publicPath": "/assets/js/5a4fc6f7.0fb85829.js" } ] }, @@ -2472,9 +2475,9 @@ "6743": { "js": [ { - "file": "assets/js/6cfa6dfb.8fd6ae54.js", - "hash": "6e8d919b6281a8cc", - "publicPath": "/assets/js/6cfa6dfb.8fd6ae54.js" + "file": "assets/js/6cfa6dfb.314d4cd8.js", + "hash": "adf1a47f3450acbd", + "publicPath": "/assets/js/6cfa6dfb.314d4cd8.js" } ] }, @@ -2544,9 +2547,9 @@ "7031": { "js": [ { - "file": "assets/js/1d75ee8e.d55ce914.js", - "hash": "46e783de32278623", - "publicPath": "/assets/js/1d75ee8e.d55ce914.js" + "file": "assets/js/1d75ee8e.be2b544e.js", + "hash": "14ec2d5fd666246b", + "publicPath": "/assets/js/1d75ee8e.be2b544e.js" } ] }, @@ -2568,6 +2571,15 @@ } ] }, + "7134": { + "js": [ + { + "file": "assets/js/e3ba908e.02341014.js", + "hash": "580dc991ae0623f4", + "publicPath": "/assets/js/e3ba908e.02341014.js" + } + ] + }, "7170": { "js": [ { @@ -2643,9 +2655,9 @@ "7486": { "js": [ { - "file": "assets/js/2c2268a5.a125bcd5.js", - "hash": "4bc96aebf5504088", - "publicPath": "/assets/js/2c2268a5.a125bcd5.js" + "file": "assets/js/2c2268a5.a41f1a69.js", + "hash": "dfa349c2274d9781", + "publicPath": "/assets/js/2c2268a5.a41f1a69.js" } ] }, @@ -2661,9 +2673,9 @@ "7682": { "js": [ { - "file": "assets/js/2b9a25fb.99463b22.js", - "hash": "9aff21da28d01c22", - "publicPath": "/assets/js/2b9a25fb.99463b22.js" + "file": "assets/js/2b9a25fb.319c9caf.js", + "hash": "d3345e7d377b12d3", + "publicPath": "/assets/js/2b9a25fb.319c9caf.js" } ] }, @@ -2697,9 +2709,9 @@ "7768": { "js": [ { - "file": "assets/js/053d6f47.c0f4d811.js", - "hash": "f16f5b8925180f63", - "publicPath": "/assets/js/053d6f47.c0f4d811.js" + "file": "assets/js/053d6f47.0d6e4e5f.js", + "hash": "d5ba526f0953a71c", + "publicPath": "/assets/js/053d6f47.0d6e4e5f.js" } ] }, @@ -2832,9 +2844,9 @@ "8279": { "js": [ { - "file": "assets/js/217dc876.22bbb9de.js", - "hash": "f55cc5894ea76f2a", - "publicPath": "/assets/js/217dc876.22bbb9de.js" + "file": "assets/js/217dc876.30c8f19d.js", + "hash": "d3f826adb0cdc931", + "publicPath": "/assets/js/217dc876.30c8f19d.js" } ] }, @@ -2922,9 +2934,9 @@ "8599": { "js": [ { - "file": "assets/js/e59bb016.99fcea04.js", - "hash": "9bd69dbf3775510c", - "publicPath": "/assets/js/e59bb016.99fcea04.js" + "file": "assets/js/e59bb016.4b2d39b4.js", + "hash": "b5aeb1abba2997f3", + "publicPath": "/assets/js/e59bb016.4b2d39b4.js" } ] }, @@ -2985,9 +2997,9 @@ "8792": { "js": [ { - "file": "assets/js/main.b3a47fe2.js", - "hash": "fd6790f479fb54c1", - "publicPath": "/assets/js/main.b3a47fe2.js" + "file": "assets/js/main.1cc4b96f.js", + "hash": "02bb4e50e1f1b31a", + "publicPath": "/assets/js/main.1cc4b96f.js" } ] }, @@ -3012,9 +3024,9 @@ "8995": { "js": [ { - "file": "assets/js/0190ef14.618325c7.js", - "hash": "9ef55dc7f9e131dd", - "publicPath": "/assets/js/0190ef14.618325c7.js" + "file": "assets/js/0190ef14.72dca7bf.js", + "hash": "99b82d821aa80b31", + "publicPath": "/assets/js/0190ef14.72dca7bf.js" } ] }, @@ -3102,9 +3114,9 @@ "9315": { "js": [ { - "file": "assets/js/f0840d59.7296e5b3.js", - "hash": "abb3279ecc22bbd7", - "publicPath": "/assets/js/f0840d59.7296e5b3.js" + "file": "assets/js/f0840d59.02df58bd.js", + "hash": "49952e6e301d139e", + "publicPath": "/assets/js/f0840d59.02df58bd.js" } ] }, @@ -3129,9 +3141,9 @@ "9525": { "js": [ { - "file": "assets/js/bf1307fc.c8a90dff.js", - "hash": "1e29b378efd2a141", - "publicPath": "/assets/js/bf1307fc.c8a90dff.js" + "file": "assets/js/bf1307fc.7b9c846a.js", + "hash": "fb6be458670b32f9", + "publicPath": "/assets/js/bf1307fc.7b9c846a.js" } ] }, @@ -3147,9 +3159,9 @@ "9714": { "js": [ { - "file": "assets/js/f66b7bee.5a87cf53.js", - "hash": "2c9fd357da009a02", - "publicPath": "/assets/js/f66b7bee.5a87cf53.js" + "file": "assets/js/f66b7bee.69fcab8e.js", + "hash": "56b1793a7bcbaeae", + "publicPath": "/assets/js/f66b7bee.69fcab8e.js" } ] }, @@ -3174,9 +3186,9 @@ "9865": { "js": [ { - "file": "assets/js/10134e3e.90eedb96.js", - "hash": "9fb18cbe97867104", - "publicPath": "/assets/js/10134e3e.90eedb96.js" + "file": "assets/js/10134e3e.d169c845.js", + "hash": "e0cbce1fe91a1a88", + "publicPath": "/assets/js/10134e3e.d169c845.js" } ] }, diff --git a/website/.docusaurus/docusaurus-plugin-content-docs/default/p/docs-next-d71.json b/website/.docusaurus/docusaurus-plugin-content-docs/default/p/docs-next-d71.json index 1aeb5d0c..afee3957 100644 --- a/website/.docusaurus/docusaurus-plugin-content-docs/default/p/docs-next-d71.json +++ b/website/.docusaurus/docusaurus-plugin-content-docs/default/p/docs-next-d71.json @@ -1 +1 @@ -{"version":{"pluginId":"default","version":"current","label":"Next","banner":"unreleased","badge":true,"noIndex":false,"className":"docs-version-current","isLast":false,"docsSidebars":{"docsSidebar":[{"type":"category","label":"Getting Started","items":[{"type":"link","label":"Overview & Philosophy","href":"/docs/next/getting-started/overview","docId":"getting-started/overview","unlisted":false},{"type":"link","label":"Installation","href":"/docs/next/getting-started/installation","docId":"getting-started/installation","unlisted":false},{"type":"link","label":"Quick Start Guide","href":"/docs/next/getting-started/quick-start","docId":"getting-started/quick-start","unlisted":false},{"type":"link","label":"Configuration & Bootstrapping","href":"/docs/next/getting-started/configuration","docId":"getting-started/configuration","unlisted":false},{"type":"link","label":"Dependency Injection","href":"/docs/next/getting-started/dependency-injection","docId":"getting-started/dependency-injection","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/getting-started"},{"type":"category","label":"Core Concepts","items":[{"type":"link","label":"Fluent Configuration","href":"/docs/next/core-concepts/fluent-configuration","docId":"core-concepts/fluent-configuration","unlisted":false},{"type":"link","label":"Guards & Validation","href":"/docs/next/core-concepts/guards","docId":"core-concepts/guards","unlisted":false},{"type":"link","label":"GUID Generation","href":"/docs/next/core-concepts/guid-generation","docId":"core-concepts/guid-generation","unlisted":false},{"type":"link","label":"System Time Abstraction","href":"/docs/next/core-concepts/system-time","docId":"core-concepts/system-time","unlisted":false},{"type":"link","label":"Execution Results & Models","href":"/docs/next/core-concepts/execution-results","docId":"core-concepts/execution-results","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/core-concepts"},{"type":"category","label":"Domain-Driven Design","items":[{"type":"link","label":"Entities & Aggregate Roots","href":"/docs/next/domain-driven-design/entities-aggregates","docId":"domain-driven-design/entities-aggregates","unlisted":false},{"type":"link","label":"Domain Events","href":"/docs/next/domain-driven-design/domain-events","docId":"domain-driven-design/domain-events","unlisted":false},{"type":"link","label":"Value Objects","href":"/docs/next/domain-driven-design/value-objects","docId":"domain-driven-design/value-objects","unlisted":false},{"type":"link","label":"Auditing","href":"/docs/next/domain-driven-design/auditing","docId":"domain-driven-design/auditing","unlisted":false},{"type":"link","label":"Soft Delete","href":"/docs/next/domain-driven-design/soft-delete","docId":"domain-driven-design/soft-delete","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/domain-driven-design"},{"type":"category","label":"Persistence","items":[{"type":"link","label":"Repository Pattern","href":"/docs/next/persistence/repository-pattern","docId":"persistence/repository-pattern","unlisted":false},{"type":"link","label":"Specifications","href":"/docs/next/persistence/specifications","docId":"persistence/specifications","unlisted":false},{"type":"link","label":"Unit of Work","href":"/docs/next/persistence/unit-of-work","docId":"persistence/unit-of-work","unlisted":false},{"type":"link","label":"Entity Framework Core","href":"/docs/next/persistence/efcore","docId":"persistence/efcore","unlisted":false},{"type":"link","label":"Dapper","href":"/docs/next/persistence/dapper","docId":"persistence/dapper","unlisted":false},{"type":"link","label":"Linq2Db","href":"/docs/next/persistence/linq2db","docId":"persistence/linq2db","unlisted":false},{"type":"link","label":"Sagas","href":"/docs/next/persistence/sagas","docId":"persistence/sagas","unlisted":false},{"type":"link","label":"Caching with Memory","href":"/docs/next/persistence/caching-memory","docId":"persistence/caching-memory","unlisted":false},{"type":"link","label":"Caching with Redis","href":"/docs/next/persistence/caching-redis","docId":"persistence/caching-redis","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/persistence"},{"type":"category","label":"CQRS & Mediator","items":[{"type":"link","label":"Command & Query Bus","href":"/docs/next/cqrs-mediator/command-query-bus","docId":"cqrs-mediator/command-query-bus","unlisted":false},{"type":"link","label":"Commands & Handlers","href":"/docs/next/cqrs-mediator/commands-handlers","docId":"cqrs-mediator/commands-handlers","unlisted":false},{"type":"link","label":"Queries & Handlers","href":"/docs/next/cqrs-mediator/queries-handlers","docId":"cqrs-mediator/queries-handlers","unlisted":false},{"type":"link","label":"MediatR","href":"/docs/next/cqrs-mediator/mediatr","docId":"cqrs-mediator/mediatr","unlisted":false},{"type":"link","label":"Wolverine","href":"/docs/next/cqrs-mediator/wolverine","docId":"cqrs-mediator/wolverine","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/cqrs--mediator"},{"type":"category","label":"Event Handling","items":[{"type":"link","label":"Overview","href":"/docs/next/event-handling/overview","docId":"event-handling/overview","unlisted":false},{"type":"link","label":"In-Memory Events","href":"/docs/next/event-handling/in-memory","docId":"event-handling/in-memory","unlisted":false},{"type":"link","label":"Distributed Events","href":"/docs/next/event-handling/distributed","docId":"event-handling/distributed","unlisted":false},{"type":"link","label":"Transactional Outbox","href":"/docs/next/event-handling/transactional-outbox","docId":"event-handling/transactional-outbox","unlisted":false},{"type":"link","label":"MediatR","href":"/docs/next/event-handling/mediatr","docId":"event-handling/mediatr","unlisted":false},{"type":"link","label":"MassTransit","href":"/docs/next/event-handling/masstransit","docId":"event-handling/masstransit","unlisted":false},{"type":"link","label":"Wolverine","href":"/docs/next/event-handling/wolverine","docId":"event-handling/wolverine","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/event-handling"},{"type":"category","label":"Messaging","items":[{"type":"link","label":"Overview","href":"/docs/next/messaging/overview","docId":"messaging/overview","unlisted":false},{"type":"link","label":"Transactional Outbox","href":"/docs/next/messaging/transactional-outbox","docId":"messaging/transactional-outbox","unlisted":false},{"type":"link","label":"State Machines","href":"/docs/next/messaging/state-machines","docId":"messaging/state-machines","unlisted":false},{"type":"link","label":"MassTransit","href":"/docs/next/messaging/masstransit","docId":"messaging/masstransit","unlisted":false},{"type":"link","label":"Wolverine","href":"/docs/next/messaging/wolverine","docId":"messaging/wolverine","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/messaging"},{"type":"category","label":"State Machines","items":[{"type":"link","label":"Overview","href":"/docs/next/state-machines/overview","docId":"state-machines/overview","unlisted":false},{"type":"link","label":"Stateless","href":"/docs/next/state-machines/stateless","docId":"state-machines/stateless","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/state-machines"},{"type":"category","label":"Caching","items":[{"type":"link","label":"Overview","href":"/docs/next/caching/overview","docId":"caching/overview","unlisted":false},{"type":"link","label":"Memory Cache","href":"/docs/next/caching/memory","docId":"caching/memory","unlisted":false},{"type":"link","label":"Redis Cache","href":"/docs/next/caching/redis","docId":"caching/redis","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/caching"},{"type":"category","label":"Blob Storage","items":[{"type":"link","label":"Overview","href":"/docs/next/blob-storage/overview","docId":"blob-storage/overview","unlisted":false},{"type":"link","label":"Azure Blob Storage","href":"/docs/next/blob-storage/azure","docId":"blob-storage/azure","unlisted":false},{"type":"link","label":"Amazon S3","href":"/docs/next/blob-storage/s3","docId":"blob-storage/s3","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/blob-storage"},{"type":"category","label":"Serialization","items":[{"type":"link","label":"Overview","href":"/docs/next/serialization/overview","docId":"serialization/overview","unlisted":false},{"type":"link","label":"Newtonsoft.Json","href":"/docs/next/serialization/newtonsoft","docId":"serialization/newtonsoft","unlisted":false},{"type":"link","label":"System.Text.Json","href":"/docs/next/serialization/system-text-json","docId":"serialization/system-text-json","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/serialization"},{"type":"category","label":"Validation","items":[{"type":"link","label":"FluentValidation","href":"/docs/next/validation/fluent-validation","docId":"validation/fluent-validation","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/validation"},{"type":"category","label":"Email","items":[{"type":"link","label":"Overview","href":"/docs/next/email/overview","docId":"email/overview","unlisted":false},{"type":"link","label":"SendGrid","href":"/docs/next/email/sendgrid","docId":"email/sendgrid","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/email"},{"type":"category","label":"Multi-Tenancy","items":[{"type":"link","label":"Overview","href":"/docs/next/multi-tenancy/overview","docId":"multi-tenancy/overview","unlisted":false},{"type":"link","label":"Finbuckle","href":"/docs/next/multi-tenancy/finbuckle","docId":"multi-tenancy/finbuckle","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/multi-tenancy"},{"type":"category","label":"Security & Web","items":[{"type":"link","label":"Authorization","href":"/docs/next/security-web/authorization","docId":"security-web/authorization","unlisted":false},{"type":"link","label":"Web Utilities","href":"/docs/next/security-web/web-utilities","docId":"security-web/web-utilities","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/security--web"},{"type":"category","label":"Architecture Guides","items":[{"type":"link","label":"Clean Architecture","href":"/docs/next/architecture-guides/clean-architecture","docId":"architecture-guides/clean-architecture","unlisted":false},{"type":"link","label":"Microservices","href":"/docs/next/architecture-guides/microservices","docId":"architecture-guides/microservices","unlisted":false},{"type":"link","label":"Event-Driven Architecture","href":"/docs/next/architecture-guides/event-driven","docId":"architecture-guides/event-driven","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/architecture-guides"},{"type":"category","label":"Examples & Recipes","items":[{"type":"link","label":"HR Leave Management Sample","href":"/docs/next/examples-recipes/hr-leave-management","docId":"examples-recipes/hr-leave-management","unlisted":false},{"type":"link","label":"Event Handling Examples","href":"/docs/next/examples-recipes/event-handling","docId":"examples-recipes/event-handling","unlisted":false},{"type":"link","label":"Caching Examples","href":"/docs/next/examples-recipes/caching","docId":"examples-recipes/caching","unlisted":false},{"type":"link","label":"Messaging Examples","href":"/docs/next/examples-recipes/messaging","docId":"examples-recipes/messaging","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/examples--recipes"},{"type":"category","label":"Testing","items":[{"type":"link","label":"Overview","href":"/docs/next/testing/overview","docId":"testing/overview","unlisted":false},{"type":"link","label":"Test Base Classes","href":"/docs/next/testing/test-base-classes","docId":"testing/test-base-classes","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/testing"},{"type":"category","label":"API Reference","items":[{"type":"link","label":"NuGet Packages","href":"/docs/next/api-reference/nuget-packages","docId":"api-reference/nuget-packages","unlisted":false},{"type":"link","label":"Changelog","href":"/docs/next/api-reference/changelog","docId":"api-reference/changelog","unlisted":false},{"type":"link","label":"Migration Guide","href":"/docs/next/api-reference/migration-guide","docId":"api-reference/migration-guide","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/api-reference"}]},"docs":{"api-reference/changelog":{"id":"api-reference/changelog","title":"Changelog","description":"RCommon release history covering blob storage abstractions, multi-tenancy support, DDD soft delete, bulk operations, and semantic versioning via MinVer git tags.","sidebar":"docsSidebar"},"api-reference/migration-guide":{"id":"api-reference/migration-guide","title":"Migration Guide","description":"Step-by-step guide for upgrading RCommon between major versions, covering repository interfaces, builder API changes, soft delete, multitenancy, and .NET framework upgrades.","sidebar":"docsSidebar"},"api-reference/nuget-packages":{"id":"api-reference/nuget-packages","title":"NuGet Packages","description":"Complete NuGet package listing for RCommon covering persistence, caching, messaging, mediator, serialization, email, security, multitenancy, and blob storage providers.","sidebar":"docsSidebar"},"architecture-guides/clean-architecture":{"id":"architecture-guides/clean-architecture","title":"Clean Architecture","description":"Build .NET apps with Clean Architecture using RCommon. Layer domain, application, and infrastructure with testable CQRS handlers and swappable persistence providers.","sidebar":"docsSidebar"},"architecture-guides/event-driven":{"id":"architecture-guides/event-driven","title":"Event-Driven Architecture","description":"Implement event-driven .NET apps with RCommon using IEventProducer and ISubscriber across in-memory, MediatR, MassTransit, and Wolverine providers.","sidebar":"docsSidebar"},"architecture-guides/microservices":{"id":"architecture-guides/microservices","title":"Microservices","description":"Build .NET microservices with RCommon using MassTransit or Wolverine for messaging, per-service persistence, and transport-agnostic IEventProducer abstractions.","sidebar":"docsSidebar"},"blob-storage/azure":{"id":"blob-storage/azure","title":"Azure Blob Storage","description":"RCommon.Azure.Blobs wraps the Azure Blob Storage SDK behind IBlobStorageService, supporting connection strings, managed identity, and multiple named stores.","sidebar":"docsSidebar"},"blob-storage/overview":{"id":"blob-storage/overview","title":"Overview","description":"Provider-agnostic blob storage abstraction for .NET covering upload, download, metadata, presigned URLs, and named multi-store registration via IBlobStoreFactory.","sidebar":"docsSidebar"},"blob-storage/s3":{"id":"blob-storage/s3","title":"Amazon S3","description":"RCommon.Amazon.S3Objects wraps the AWS SDK behind IBlobStorageService, supporting explicit credentials, named profiles, IAM instance roles, and S3-compatible stores.","sidebar":"docsSidebar"},"caching/memory":{"id":"caching/memory","title":"Memory Cache","description":"RCommon.MemoryCache provides in-process IMemoryCache and IDistributedCache-backed caching with get-or-create patterns and LINQ expression caching optimization.","sidebar":"docsSidebar"},"caching/overview":{"id":"caching/overview","title":"Overview","description":"Provider-agnostic caching abstraction for .NET with ICacheService, strongly-typed CacheKey, and support for in-memory, distributed, and Redis backends.","sidebar":"docsSidebar"},"caching/redis":{"id":"caching/redis","title":"Redis Cache","description":"RCommon.RedisCache implements ICacheService using StackExchange.Redis for distributed caching across multiple app instances, with JSON serialization support.","sidebar":"docsSidebar"},"core-concepts/execution-results":{"id":"core-concepts/execution-results","title":"Execution Results & Models","description":"Return success or failure using RCommon's ExecutionResult pattern, and paginate data with PagedResult and PaginatedListModel — structured results without exceptions.","sidebar":"docsSidebar"},"core-concepts/fluent-configuration":{"id":"core-concepts/fluent-configuration","title":"Fluent Configuration","description":"Deep dive into RCommon's fluent configuration API — IRCommonBuilder, With* extension methods, custom builder extensions, and double-registration protection.","sidebar":"docsSidebar"},"core-concepts/guards":{"id":"core-concepts/guards","title":"Guards & Validation","description":"Use RCommon Guard clauses for defensive programming — null checks, string, GUID, numeric, date, collection, type, and custom condition validation in .NET.","sidebar":"docsSidebar"},"core-concepts/guid-generation":{"id":"core-concepts/guid-generation","title":"GUID Generation","description":"RCommon's IGuidGenerator abstraction for testable GUID creation — sequential GUIDs for SQL Server, MySQL, PostgreSQL, and Oracle to reduce index fragmentation.","sidebar":"docsSidebar"},"core-concepts/system-time":{"id":"core-concepts/system-time","title":"System Time Abstraction","description":"Use ISystemTime to abstract DateTime.Now for testable time-dependent code — configure UTC or local time, normalize incoming dates, and substitute clocks in tests.","sidebar":"docsSidebar"},"cqrs-mediator/command-query-bus":{"id":"cqrs-mediator/command-query-bus","title":"Command & Query Bus","description":"Learn how RCommon's ICommandBus and IQueryBus implement CQRS, resolve handlers from DI, support validation, and cache compiled dispatch delegates.","sidebar":"docsSidebar"},"cqrs-mediator/commands-handlers":{"id":"cqrs-mediator/commands-handlers","title":"Commands & Handlers","description":"Define ICommand classes, implement ICommandHandler, register handlers individually or by assembly scan, and add FluentValidation before dispatch.","sidebar":"docsSidebar"},"cqrs-mediator/mediatr":{"id":"cqrs-mediator/mediatr","title":"MediatR","description":"Wire MediatR into RCommon's mediator abstraction for request-response and fan-out notifications, with built-in logging, validation, and unit-of-work pipeline behaviors.","sidebar":"docsSidebar"},"cqrs-mediator/queries-handlers":{"id":"cqrs-mediator/queries-handlers","title":"Queries & Handlers","description":"Define read-only IQuery classes, implement IQueryHandler, register via assembly scan or explicit registration, and optionally validate queries before dispatch.","sidebar":"docsSidebar"},"cqrs-mediator/wolverine":{"id":"cqrs-mediator/wolverine","title":"Wolverine","description":"Integrate WolverineFx as a distributed event producer in RCommon, supporting fan-out publish and point-to-point send via ISubscriber without Wolverine-specific handler code.","sidebar":"docsSidebar"},"domain-driven-design/auditing":{"id":"domain-driven-design/auditing","title":"Auditing","description":"Capture who created and last modified an entity using RCommon's IAuditedEntity interface and AuditedEntity base classes with generic user identifier types.","sidebar":"docsSidebar"},"domain-driven-design/domain-events":{"id":"domain-driven-design/domain-events","title":"Domain Events","description":"Raise, accumulate, and dispatch domain events in RCommon using the DomainEvent base record, ISubscriber handlers, and the post-persistence event routing pipeline.","sidebar":"docsSidebar"},"domain-driven-design/entities-aggregates":{"id":"domain-driven-design/entities-aggregates","title":"Entities & Aggregate Roots","description":"Learn how RCommon's BusinessEntity and AggregateRoot base classes give your domain model typed identity, domain event tracking, and optimistic concurrency out of the box.","sidebar":"docsSidebar"},"domain-driven-design/soft-delete":{"id":"domain-driven-design/soft-delete","title":"Soft Delete","description":"Mark records as deleted instead of removing them physically using RCommon's ISoftDelete interface, with automatic query filtering and explicit delete mode overrides.","sidebar":"docsSidebar"},"domain-driven-design/value-objects":{"id":"domain-driven-design/value-objects","title":"Value Objects","description":"Model domain concepts by value rather than identity using RCommon's ValueObject and ValueObject<T> abstract records with built-in structural equality and immutability.","sidebar":"docsSidebar"},"email/overview":{"id":"email/overview","title":"Overview","description":"Provider-agnostic email abstraction for .NET with IEmailService, built-in SMTP support via SmtpClient, HTML bodies, attachments, and an EmailSent event.","sidebar":"docsSidebar"},"email/sendgrid":{"id":"email/sendgrid","title":"SendGrid","description":"RCommon.SendGrid implements IEmailService via the SendGrid API, converting MailMessage objects to SendGrid calls with attachment and multi-recipient support.","sidebar":"docsSidebar"},"event-handling/distributed":{"id":"event-handling/distributed","title":"Distributed Events","description":"Extend RCommon event handling to message brokers with MassTransit or Wolverine, mixing transports in one app while keeping ISubscriber handler code broker-agnostic.","sidebar":"docsSidebar"},"event-handling/in-memory":{"id":"event-handling/in-memory","title":"In-Memory Events","description":"Configure RCommon's in-process event bus with InMemoryEventBusBuilder, implement ISubscriber handlers, and publish events via IEventRouter or IEventBus directly.","sidebar":"docsSidebar"},"event-handling/masstransit":{"id":"event-handling/masstransit","title":"MassTransit","description":"Deliver RCommon events across service boundaries via MassTransit consumers, with fan-out publish or point-to-point send and transport-agnostic ISubscriber handlers.","sidebar":"docsSidebar"},"event-handling/mediatr":{"id":"event-handling/mediatr","title":"MediatR","description":"Route RCommon events through MediatR's notification pipeline using MediatREventHandlingBuilder, with ISubscriber handlers unaware of MediatR internals.","sidebar":"docsSidebar"},"event-handling/overview":{"id":"event-handling/overview","title":"Overview","description":"Understand RCommon's unified event handling pipeline — IEventBus, IEventProducer, IEventRouter, and ISubscriber — and how to choose between in-process and broker transports.","sidebar":"docsSidebar"},"event-handling/transactional-outbox":{"id":"event-handling/transactional-outbox","title":"Transactional Outbox","description":"Guarantee at-least-once event delivery using the transactional outbox pattern with MassTransit EF Core outbox or Wolverine's EF Core transaction integration.","sidebar":"docsSidebar"},"event-handling/wolverine":{"id":"event-handling/wolverine","title":"Wolverine","description":"Use Wolverine as an event transport in RCommon's event handling pipeline, registering ISubscriber handlers and choosing between fan-out publish or point-to-point send.","sidebar":"docsSidebar"},"examples-recipes/caching":{"id":"examples-recipes/caching","title":"Caching Examples","description":"Practical RCommon caching examples covering IMemoryCache, distributed memory cache, Redis, and ICachingGraphRepository for caching EF Core repository query results.","sidebar":"docsSidebar"},"examples-recipes/event-handling":{"id":"examples-recipes/event-handling","title":"Event Handling Examples","description":"Step-by-step event handling examples for RCommon using InMemoryEventBus and MediatR providers with ISyncEvent, IEventProducer, and ISubscriber patterns.","sidebar":"docsSidebar"},"examples-recipes/hr-leave-management":{"id":"examples-recipes/hr-leave-management","title":"HR Leave Management Sample","description":"Explore RCommon's HR Leave Management reference app demonstrating Clean Architecture, CQRS, EF Core, FluentValidation, JWT auth, and SendGrid email integration.","sidebar":"docsSidebar"},"examples-recipes/messaging":{"id":"examples-recipes/messaging","title":"Messaging Examples","description":"Messaging examples for RCommon showing MassTransit and Wolverine transports, subscription isolation between multiple builders, and in-process vs distributed routing.","sidebar":"docsSidebar"},"getting-started/configuration":{"id":"getting-started/configuration","title":"Configuration & Bootstrapping","description":"Configure RCommon with AddRCommon() — fluent builder methods for GUID generation, system time, persistence, unit of work, mediator, and event handling.","sidebar":"docsSidebar"},"getting-started/dependency-injection":{"id":"getting-started/dependency-injection","title":"Dependency Injection","description":"How RCommon integrates with Microsoft.Extensions.DependencyInjection — service lifetimes, injecting repositories, IEventBus, ISystemTime, and named data stores.","sidebar":"docsSidebar"},"getting-started/installation":{"id":"getting-started/installation","title":"Installation","description":"Install RCommon NuGet packages for .NET 8, 9, or 10. Pick only what you need — persistence, CQRS, messaging, caching, validation, blob storage, and more.","sidebar":"docsSidebar"},"getting-started/overview":{"id":"getting-started/overview","title":"Overview & Philosophy","description":"Learn what RCommon provides — pluggable .NET abstractions for persistence, CQRS, domain events, messaging, and caching with a fluent DI-first configuration API.","sidebar":"docsSidebar"},"getting-started/quick-start":{"id":"getting-started/quick-start","title":"Quick Start Guide","description":"Build a .NET web API with RCommon in minutes — define entities, configure EF Core persistence, and query via ILinqRepository using a single fluent builder chain.","sidebar":"docsSidebar"},"index":{"id":"index","title":"RCommon Documentation","description":"Complete documentation for RCommon — a .NET infrastructure library with persistence, CQRS, event handling, messaging, caching, and validation abstractions."},"messaging/masstransit":{"id":"messaging/masstransit","title":"MassTransit","description":"Configure MassTransit as RCommon's messaging transport, register consumers and producers, choose RabbitMQ or Azure Service Bus, and keep handler code broker-free.","sidebar":"docsSidebar"},"messaging/overview":{"id":"messaging/overview","title":"Overview","description":"Overview of RCommon's broker-backed messaging layer built on IEventRouter and IEventProducer, comparing MassTransit and Wolverine transports for cross-service delivery.","sidebar":"docsSidebar"},"messaging/state-machines":{"id":"messaging/state-machines","title":"State Machines","description":"Orchestrate long-running workflows in RCommon using MassTransit's dictionary-based state machine adapter with IStateMachineConfigurator, guards, and async entry/exit actions.","sidebar":"docsSidebar"},"messaging/transactional-outbox":{"id":"messaging/transactional-outbox","title":"Transactional Outbox","description":"Ensure messages reach the broker only when the database transaction commits using the transactional outbox for MassTransit or Wolverine with EF Core integration.","sidebar":"docsSidebar"},"messaging/wolverine":{"id":"messaging/wolverine","title":"Wolverine","description":"Configure Wolverine as RCommon's messaging transport with durable delivery, fan-out publish or point-to-point send, and ISubscriber handlers free of Wolverine dependencies.","sidebar":"docsSidebar"},"multi-tenancy/finbuckle":{"id":"multi-tenancy/finbuckle","title":"Finbuckle","description":"RCommon.Finbuckle integrates Finbuckle.MultiTenant with RCommon's persistence layer, automatically scoping queries and entity writes to the request-resolved tenant.","sidebar":"docsSidebar"},"multi-tenancy/overview":{"id":"multi-tenancy/overview","title":"Overview","description":"Multi-tenancy abstraction for RCommon that automatically scopes repository queries and entity stamps to the active tenant via ITenantIdAccessor and IMultiTenant.","sidebar":"docsSidebar"},"persistence/caching-memory":{"id":"persistence/caching-memory","title":"Caching with Memory","description":"Decorate RCommon repositories with in-process memory caching using ICachingGraphRepository and caller-supplied cache keys, with write operations always bypassing the cache.","sidebar":"docsSidebar"},"persistence/caching-redis":{"id":"persistence/caching-redis","title":"Caching with Redis","description":"Back RCommon's repository caching decorators with Redis via RedisCacheService for a distributed, out-of-process cache shared across multiple service instances and restarts.","sidebar":"docsSidebar"},"persistence/dapper":{"id":"persistence/dapper","title":"Dapper","description":"Use RCommon's Dapper provider for lightweight SQL data access with Dommel-generated queries, raw SQL via ISqlMapperRepository, and automatic soft-delete handling.","sidebar":"docsSidebar"},"persistence/efcore":{"id":"persistence/efcore","title":"Entity Framework Core","description":"Configure RCommon's EF Core provider to register named DbContexts, inject IGraphRepository with full LINQ support, eager loading, soft delete, and change-tracking control.","sidebar":"docsSidebar"},"persistence/linq2db":{"id":"persistence/linq2db","title":"Linq2Db","description":"Wire up RCommon's Linq2Db provider for ILinqRepository support with LINQ queries, paging, eager loading via LoadWith, and multi-database connection management without EF Core.","sidebar":"docsSidebar"},"persistence/repository-pattern":{"id":"persistence/repository-pattern","title":"Repository Pattern","description":"Use RCommon's provider-agnostic repository interfaces for async CRUD, paging, eager loading, and soft delete across EF Core, Dapper, and Linq2Db without coupling to a specific ORM.","sidebar":"docsSidebar"},"persistence/sagas":{"id":"persistence/sagas","title":"Sagas","description":"Orchestrate long-running business processes with RCommon's saga framework using state machines, durable SagaState persistence, correlation IDs, and automatic compensation.","sidebar":"docsSidebar"},"persistence/specifications":{"id":"persistence/specifications","title":"Specifications","description":"Encapsulate reusable query predicates as named Specification objects, compose them with & and | operators, and pass them directly to any RCommon repository method.","sidebar":"docsSidebar"},"persistence/unit-of-work":{"id":"persistence/unit-of-work","title":"Unit of Work","description":"Coordinate writes across multiple repositories in a single TransactionScope using RCommon's IUnitOfWorkFactory, with MediatR and Wolverine pipeline integration support.","sidebar":"docsSidebar"},"security-web/authorization":{"id":"security-web/authorization","title":"Authorization","description":"RCommon.Security provides ICurrentUser and ICurrentPrincipalAccessor for claims-based identity resolution, plus Swashbuckle filters for OpenAPI authorization docs.","sidebar":"docsSidebar"},"security-web/web-utilities":{"id":"security-web/web-utilities","title":"Web Utilities","description":"RCommon.Web bridges ASP.NET Core's HttpContext.User to ICurrentPrincipalAccessor, replacing the thread-based default for correct identity resolution in web apps.","sidebar":"docsSidebar"},"serialization/newtonsoft":{"id":"serialization/newtonsoft","title":"Newtonsoft.Json","description":"RCommon.JsonNet wraps Newtonsoft.Json behind IJsonSerializer with full JsonSerializerSettings access, custom converters, and camelCase or indented output options.","sidebar":"docsSidebar"},"serialization/overview":{"id":"serialization/overview","title":"Overview","description":"Provider-agnostic JSON serialization for .NET with IJsonSerializer, supporting Newtonsoft.Json and System.Text.Json with per-call and global option configuration.","sidebar":"docsSidebar"},"serialization/system-text-json":{"id":"serialization/system-text-json","title":"System.Text.Json","description":"RCommon.SystemTextJson wraps System.Text.Json behind IJsonSerializer with full JsonSerializerOptions access and built-in integer and byte enum converters.","sidebar":"docsSidebar"},"state-machines/overview":{"id":"state-machines/overview","title":"Overview","description":"Provider-agnostic finite state machine abstraction for .NET using C# enums, with Stateless and MassTransit adapter options for workflow and lifecycle modeling.","sidebar":"docsSidebar"},"state-machines/stateless":{"id":"state-machines/stateless","title":"Stateless","description":"RCommon.Stateless wraps the Stateless library behind IStateMachineConfigurator, supporting guarded transitions, parameterized triggers, and async entry/exit actions.","sidebar":"docsSidebar"},"testing/overview":{"id":"testing/overview","title":"Overview","description":"Learn how to test .NET applications built on RCommon using mocks, EF Core InMemory, and xUnit or NUnit base classes for unit and integration test strategies.","sidebar":"docsSidebar"},"testing/test-base-classes":{"id":"testing/test-base-classes","title":"Test Base Classes","description":"Reference guide for RCommon.TestBase, TestBootstrapper, TestFixture for xUnit, TestRepository for EF Core integration tests, and TestDataActions fake data generation.","sidebar":"docsSidebar"},"validation/fluent-validation":{"id":"validation/fluent-validation","title":"FluentValidation","description":"RCommon.FluentValidation integrates FluentValidation with the CQRS pipeline for automatic command and query validation, with manual IValidationService support.","sidebar":"docsSidebar"}}}} \ No newline at end of file +{"version":{"pluginId":"default","version":"current","label":"Next","banner":"unreleased","badge":true,"noIndex":false,"className":"docs-version-current","isLast":false,"docsSidebars":{"docsSidebar":[{"type":"category","label":"Getting Started","items":[{"type":"link","label":"Overview & Philosophy","href":"/docs/next/getting-started/overview","docId":"getting-started/overview","unlisted":false},{"type":"link","label":"Installation","href":"/docs/next/getting-started/installation","docId":"getting-started/installation","unlisted":false},{"type":"link","label":"Quick Start Guide","href":"/docs/next/getting-started/quick-start","docId":"getting-started/quick-start","unlisted":false},{"type":"link","label":"Configuration & Bootstrapping","href":"/docs/next/getting-started/configuration","docId":"getting-started/configuration","unlisted":false},{"type":"link","label":"Dependency Injection","href":"/docs/next/getting-started/dependency-injection","docId":"getting-started/dependency-injection","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/getting-started"},{"type":"category","label":"Core Concepts","items":[{"type":"link","label":"Fluent Configuration","href":"/docs/next/core-concepts/fluent-configuration","docId":"core-concepts/fluent-configuration","unlisted":false},{"type":"link","label":"Modular Composition","href":"/docs/next/core-concepts/modular-composition","docId":"core-concepts/modular-composition","unlisted":false},{"type":"link","label":"Guards & Validation","href":"/docs/next/core-concepts/guards","docId":"core-concepts/guards","unlisted":false},{"type":"link","label":"GUID Generation","href":"/docs/next/core-concepts/guid-generation","docId":"core-concepts/guid-generation","unlisted":false},{"type":"link","label":"System Time Abstraction","href":"/docs/next/core-concepts/system-time","docId":"core-concepts/system-time","unlisted":false},{"type":"link","label":"Execution Results & Models","href":"/docs/next/core-concepts/execution-results","docId":"core-concepts/execution-results","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/core-concepts"},{"type":"category","label":"Domain-Driven Design","items":[{"type":"link","label":"Entities & Aggregate Roots","href":"/docs/next/domain-driven-design/entities-aggregates","docId":"domain-driven-design/entities-aggregates","unlisted":false},{"type":"link","label":"Domain Events","href":"/docs/next/domain-driven-design/domain-events","docId":"domain-driven-design/domain-events","unlisted":false},{"type":"link","label":"Value Objects","href":"/docs/next/domain-driven-design/value-objects","docId":"domain-driven-design/value-objects","unlisted":false},{"type":"link","label":"Auditing","href":"/docs/next/domain-driven-design/auditing","docId":"domain-driven-design/auditing","unlisted":false},{"type":"link","label":"Soft Delete","href":"/docs/next/domain-driven-design/soft-delete","docId":"domain-driven-design/soft-delete","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/domain-driven-design"},{"type":"category","label":"Persistence","items":[{"type":"link","label":"Repository Pattern","href":"/docs/next/persistence/repository-pattern","docId":"persistence/repository-pattern","unlisted":false},{"type":"link","label":"Specifications","href":"/docs/next/persistence/specifications","docId":"persistence/specifications","unlisted":false},{"type":"link","label":"Unit of Work","href":"/docs/next/persistence/unit-of-work","docId":"persistence/unit-of-work","unlisted":false},{"type":"link","label":"Entity Framework Core","href":"/docs/next/persistence/efcore","docId":"persistence/efcore","unlisted":false},{"type":"link","label":"Dapper","href":"/docs/next/persistence/dapper","docId":"persistence/dapper","unlisted":false},{"type":"link","label":"Linq2Db","href":"/docs/next/persistence/linq2db","docId":"persistence/linq2db","unlisted":false},{"type":"link","label":"Sagas","href":"/docs/next/persistence/sagas","docId":"persistence/sagas","unlisted":false},{"type":"link","label":"Caching with Memory","href":"/docs/next/persistence/caching-memory","docId":"persistence/caching-memory","unlisted":false},{"type":"link","label":"Caching with Redis","href":"/docs/next/persistence/caching-redis","docId":"persistence/caching-redis","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/persistence"},{"type":"category","label":"CQRS & Mediator","items":[{"type":"link","label":"Command & Query Bus","href":"/docs/next/cqrs-mediator/command-query-bus","docId":"cqrs-mediator/command-query-bus","unlisted":false},{"type":"link","label":"Commands & Handlers","href":"/docs/next/cqrs-mediator/commands-handlers","docId":"cqrs-mediator/commands-handlers","unlisted":false},{"type":"link","label":"Queries & Handlers","href":"/docs/next/cqrs-mediator/queries-handlers","docId":"cqrs-mediator/queries-handlers","unlisted":false},{"type":"link","label":"MediatR","href":"/docs/next/cqrs-mediator/mediatr","docId":"cqrs-mediator/mediatr","unlisted":false},{"type":"link","label":"Wolverine","href":"/docs/next/cqrs-mediator/wolverine","docId":"cqrs-mediator/wolverine","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/cqrs--mediator"},{"type":"category","label":"Event Handling","items":[{"type":"link","label":"Overview","href":"/docs/next/event-handling/overview","docId":"event-handling/overview","unlisted":false},{"type":"link","label":"In-Memory Events","href":"/docs/next/event-handling/in-memory","docId":"event-handling/in-memory","unlisted":false},{"type":"link","label":"Distributed Events","href":"/docs/next/event-handling/distributed","docId":"event-handling/distributed","unlisted":false},{"type":"link","label":"Transactional Outbox","href":"/docs/next/event-handling/transactional-outbox","docId":"event-handling/transactional-outbox","unlisted":false},{"type":"link","label":"MediatR","href":"/docs/next/event-handling/mediatr","docId":"event-handling/mediatr","unlisted":false},{"type":"link","label":"MassTransit","href":"/docs/next/event-handling/masstransit","docId":"event-handling/masstransit","unlisted":false},{"type":"link","label":"Wolverine","href":"/docs/next/event-handling/wolverine","docId":"event-handling/wolverine","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/event-handling"},{"type":"category","label":"Messaging","items":[{"type":"link","label":"Overview","href":"/docs/next/messaging/overview","docId":"messaging/overview","unlisted":false},{"type":"link","label":"Transactional Outbox","href":"/docs/next/messaging/transactional-outbox","docId":"messaging/transactional-outbox","unlisted":false},{"type":"link","label":"State Machines","href":"/docs/next/messaging/state-machines","docId":"messaging/state-machines","unlisted":false},{"type":"link","label":"MassTransit","href":"/docs/next/messaging/masstransit","docId":"messaging/masstransit","unlisted":false},{"type":"link","label":"Wolverine","href":"/docs/next/messaging/wolverine","docId":"messaging/wolverine","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/messaging"},{"type":"category","label":"State Machines","items":[{"type":"link","label":"Overview","href":"/docs/next/state-machines/overview","docId":"state-machines/overview","unlisted":false},{"type":"link","label":"Stateless","href":"/docs/next/state-machines/stateless","docId":"state-machines/stateless","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/state-machines"},{"type":"category","label":"Caching","items":[{"type":"link","label":"Overview","href":"/docs/next/caching/overview","docId":"caching/overview","unlisted":false},{"type":"link","label":"Memory Cache","href":"/docs/next/caching/memory","docId":"caching/memory","unlisted":false},{"type":"link","label":"Redis Cache","href":"/docs/next/caching/redis","docId":"caching/redis","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/caching"},{"type":"category","label":"Blob Storage","items":[{"type":"link","label":"Overview","href":"/docs/next/blob-storage/overview","docId":"blob-storage/overview","unlisted":false},{"type":"link","label":"Azure Blob Storage","href":"/docs/next/blob-storage/azure","docId":"blob-storage/azure","unlisted":false},{"type":"link","label":"Amazon S3","href":"/docs/next/blob-storage/s3","docId":"blob-storage/s3","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/blob-storage"},{"type":"category","label":"Serialization","items":[{"type":"link","label":"Overview","href":"/docs/next/serialization/overview","docId":"serialization/overview","unlisted":false},{"type":"link","label":"Newtonsoft.Json","href":"/docs/next/serialization/newtonsoft","docId":"serialization/newtonsoft","unlisted":false},{"type":"link","label":"System.Text.Json","href":"/docs/next/serialization/system-text-json","docId":"serialization/system-text-json","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/serialization"},{"type":"category","label":"Validation","items":[{"type":"link","label":"FluentValidation","href":"/docs/next/validation/fluent-validation","docId":"validation/fluent-validation","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/validation"},{"type":"category","label":"Email","items":[{"type":"link","label":"Overview","href":"/docs/next/email/overview","docId":"email/overview","unlisted":false},{"type":"link","label":"SendGrid","href":"/docs/next/email/sendgrid","docId":"email/sendgrid","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/email"},{"type":"category","label":"Multi-Tenancy","items":[{"type":"link","label":"Overview","href":"/docs/next/multi-tenancy/overview","docId":"multi-tenancy/overview","unlisted":false},{"type":"link","label":"Finbuckle","href":"/docs/next/multi-tenancy/finbuckle","docId":"multi-tenancy/finbuckle","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/multi-tenancy"},{"type":"category","label":"Security & Web","items":[{"type":"link","label":"Authorization","href":"/docs/next/security-web/authorization","docId":"security-web/authorization","unlisted":false},{"type":"link","label":"Web Utilities","href":"/docs/next/security-web/web-utilities","docId":"security-web/web-utilities","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/security--web"},{"type":"category","label":"Architecture Guides","items":[{"type":"link","label":"Clean Architecture","href":"/docs/next/architecture-guides/clean-architecture","docId":"architecture-guides/clean-architecture","unlisted":false},{"type":"link","label":"Microservices","href":"/docs/next/architecture-guides/microservices","docId":"architecture-guides/microservices","unlisted":false},{"type":"link","label":"Event-Driven Architecture","href":"/docs/next/architecture-guides/event-driven","docId":"architecture-guides/event-driven","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/architecture-guides"},{"type":"category","label":"Examples & Recipes","items":[{"type":"link","label":"HR Leave Management Sample","href":"/docs/next/examples-recipes/hr-leave-management","docId":"examples-recipes/hr-leave-management","unlisted":false},{"type":"link","label":"Event Handling Examples","href":"/docs/next/examples-recipes/event-handling","docId":"examples-recipes/event-handling","unlisted":false},{"type":"link","label":"Caching Examples","href":"/docs/next/examples-recipes/caching","docId":"examples-recipes/caching","unlisted":false},{"type":"link","label":"Messaging Examples","href":"/docs/next/examples-recipes/messaging","docId":"examples-recipes/messaging","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/examples--recipes"},{"type":"category","label":"Testing","items":[{"type":"link","label":"Overview","href":"/docs/next/testing/overview","docId":"testing/overview","unlisted":false},{"type":"link","label":"Test Base Classes","href":"/docs/next/testing/test-base-classes","docId":"testing/test-base-classes","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/testing"},{"type":"category","label":"API Reference","items":[{"type":"link","label":"NuGet Packages","href":"/docs/next/api-reference/nuget-packages","docId":"api-reference/nuget-packages","unlisted":false},{"type":"link","label":"Changelog","href":"/docs/next/api-reference/changelog","docId":"api-reference/changelog","unlisted":false},{"type":"link","label":"Migration Guide","href":"/docs/next/api-reference/migration-guide","docId":"api-reference/migration-guide","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/api-reference"}]},"docs":{"api-reference/changelog":{"id":"api-reference/changelog","title":"Changelog","description":"RCommon release history covering blob storage abstractions, multi-tenancy support, DDD soft delete, bulk operations, and semantic versioning via MinVer git tags.","sidebar":"docsSidebar"},"api-reference/migration-guide":{"id":"api-reference/migration-guide","title":"Migration Guide","description":"Step-by-step guide for upgrading RCommon between major versions, covering repository interfaces, builder API changes, soft delete, multitenancy, and .NET framework upgrades.","sidebar":"docsSidebar"},"api-reference/nuget-packages":{"id":"api-reference/nuget-packages","title":"NuGet Packages","description":"Complete NuGet package listing for RCommon covering persistence, caching, messaging, mediator, serialization, email, security, multitenancy, and blob storage providers.","sidebar":"docsSidebar"},"architecture-guides/clean-architecture":{"id":"architecture-guides/clean-architecture","title":"Clean Architecture","description":"Build .NET apps with Clean Architecture using RCommon. Layer domain, application, and infrastructure with testable CQRS handlers and swappable persistence providers.","sidebar":"docsSidebar"},"architecture-guides/event-driven":{"id":"architecture-guides/event-driven","title":"Event-Driven Architecture","description":"Implement event-driven .NET apps with RCommon using IEventProducer and ISubscriber across in-memory, MediatR, MassTransit, and Wolverine providers.","sidebar":"docsSidebar"},"architecture-guides/microservices":{"id":"architecture-guides/microservices","title":"Microservices","description":"Build .NET microservices with RCommon using MassTransit or Wolverine for messaging, per-service persistence, and transport-agnostic IEventProducer abstractions.","sidebar":"docsSidebar"},"blob-storage/azure":{"id":"blob-storage/azure","title":"Azure Blob Storage","description":"RCommon.Azure.Blobs wraps the Azure Blob Storage SDK behind IBlobStorageService, supporting connection strings, managed identity, and multiple named stores.","sidebar":"docsSidebar"},"blob-storage/overview":{"id":"blob-storage/overview","title":"Overview","description":"Provider-agnostic blob storage abstraction for .NET covering upload, download, metadata, presigned URLs, and named multi-store registration via IBlobStoreFactory.","sidebar":"docsSidebar"},"blob-storage/s3":{"id":"blob-storage/s3","title":"Amazon S3","description":"RCommon.Amazon.S3Objects wraps the AWS SDK behind IBlobStorageService, supporting explicit credentials, named profiles, IAM instance roles, and S3-compatible stores.","sidebar":"docsSidebar"},"caching/memory":{"id":"caching/memory","title":"Memory Cache","description":"RCommon.MemoryCache provides in-process IMemoryCache and IDistributedCache-backed caching with get-or-create patterns and LINQ expression caching optimization.","sidebar":"docsSidebar"},"caching/overview":{"id":"caching/overview","title":"Overview","description":"Provider-agnostic caching abstraction for .NET with ICacheService, strongly-typed CacheKey, and support for in-memory, distributed, and Redis backends.","sidebar":"docsSidebar"},"caching/redis":{"id":"caching/redis","title":"Redis Cache","description":"RCommon.RedisCache implements ICacheService using StackExchange.Redis for distributed caching across multiple app instances, with JSON serialization support.","sidebar":"docsSidebar"},"core-concepts/execution-results":{"id":"core-concepts/execution-results","title":"Execution Results & Models","description":"Return success or failure using RCommon's ExecutionResult pattern, and paginate data with PagedResult and PaginatedListModel — structured results without exceptions.","sidebar":"docsSidebar"},"core-concepts/fluent-configuration":{"id":"core-concepts/fluent-configuration","title":"Fluent Configuration","description":"Deep dive into RCommon's fluent configuration API — IRCommonBuilder, With* extension methods, custom builder extensions, and double-registration protection.","sidebar":"docsSidebar"},"core-concepts/guards":{"id":"core-concepts/guards","title":"Guards & Validation","description":"Use RCommon Guard clauses for defensive programming — null checks, string, GUID, numeric, date, collection, type, and custom condition validation in .NET.","sidebar":"docsSidebar"},"core-concepts/guid-generation":{"id":"core-concepts/guid-generation","title":"GUID Generation","description":"RCommon's IGuidGenerator abstraction for testable GUID creation — sequential GUIDs for SQL Server, MySQL, PostgreSQL, and Oracle to reduce index fragmentation.","sidebar":"docsSidebar"},"core-concepts/modular-composition":{"id":"core-concepts/modular-composition","title":"Modular Composition","description":"Compose AddRCommon across multiple modules in a single process — registrations merge, never duplicate or throw on agreement.","sidebar":"docsSidebar"},"core-concepts/system-time":{"id":"core-concepts/system-time","title":"System Time Abstraction","description":"Use ISystemTime to abstract DateTime.Now for testable time-dependent code — configure UTC or local time, normalize incoming dates, and substitute clocks in tests.","sidebar":"docsSidebar"},"cqrs-mediator/command-query-bus":{"id":"cqrs-mediator/command-query-bus","title":"Command & Query Bus","description":"Learn how RCommon's ICommandBus and IQueryBus implement CQRS, resolve handlers from DI, support validation, and cache compiled dispatch delegates.","sidebar":"docsSidebar"},"cqrs-mediator/commands-handlers":{"id":"cqrs-mediator/commands-handlers","title":"Commands & Handlers","description":"Define ICommand classes, implement ICommandHandler, register handlers individually or by assembly scan, and add FluentValidation before dispatch.","sidebar":"docsSidebar"},"cqrs-mediator/mediatr":{"id":"cqrs-mediator/mediatr","title":"MediatR","description":"Wire MediatR into RCommon's mediator abstraction for request-response and fan-out notifications, with built-in logging, validation, and unit-of-work pipeline behaviors.","sidebar":"docsSidebar"},"cqrs-mediator/queries-handlers":{"id":"cqrs-mediator/queries-handlers","title":"Queries & Handlers","description":"Define read-only IQuery classes, implement IQueryHandler, register via assembly scan or explicit registration, and optionally validate queries before dispatch.","sidebar":"docsSidebar"},"cqrs-mediator/wolverine":{"id":"cqrs-mediator/wolverine","title":"Wolverine","description":"Integrate WolverineFx as a distributed event producer in RCommon, supporting fan-out publish and point-to-point send via ISubscriber without Wolverine-specific handler code.","sidebar":"docsSidebar"},"domain-driven-design/auditing":{"id":"domain-driven-design/auditing","title":"Auditing","description":"Capture who created and last modified an entity using RCommon's IAuditedEntity interface and AuditedEntity base classes with generic user identifier types.","sidebar":"docsSidebar"},"domain-driven-design/domain-events":{"id":"domain-driven-design/domain-events","title":"Domain Events","description":"Raise, accumulate, and dispatch domain events in RCommon using the DomainEvent base record, ISubscriber handlers, and the post-persistence event routing pipeline.","sidebar":"docsSidebar"},"domain-driven-design/entities-aggregates":{"id":"domain-driven-design/entities-aggregates","title":"Entities & Aggregate Roots","description":"Learn how RCommon's BusinessEntity and AggregateRoot base classes give your domain model typed identity, domain event tracking, and optimistic concurrency out of the box.","sidebar":"docsSidebar"},"domain-driven-design/soft-delete":{"id":"domain-driven-design/soft-delete","title":"Soft Delete","description":"Mark records as deleted instead of removing them physically using RCommon's ISoftDelete interface, with automatic query filtering and explicit delete mode overrides.","sidebar":"docsSidebar"},"domain-driven-design/value-objects":{"id":"domain-driven-design/value-objects","title":"Value Objects","description":"Model domain concepts by value rather than identity using RCommon's ValueObject and ValueObject<T> abstract records with built-in structural equality and immutability.","sidebar":"docsSidebar"},"email/overview":{"id":"email/overview","title":"Overview","description":"Provider-agnostic email abstraction for .NET with IEmailService, built-in SMTP support via SmtpClient, HTML bodies, attachments, and an EmailSent event.","sidebar":"docsSidebar"},"email/sendgrid":{"id":"email/sendgrid","title":"SendGrid","description":"RCommon.SendGrid implements IEmailService via the SendGrid API, converting MailMessage objects to SendGrid calls with attachment and multi-recipient support.","sidebar":"docsSidebar"},"event-handling/distributed":{"id":"event-handling/distributed","title":"Distributed Events","description":"Extend RCommon event handling to message brokers with MassTransit or Wolverine, mixing transports in one app while keeping ISubscriber handler code broker-agnostic.","sidebar":"docsSidebar"},"event-handling/in-memory":{"id":"event-handling/in-memory","title":"In-Memory Events","description":"Configure RCommon's in-process event bus with InMemoryEventBusBuilder, implement ISubscriber handlers, and publish events via IEventRouter or IEventBus directly.","sidebar":"docsSidebar"},"event-handling/masstransit":{"id":"event-handling/masstransit","title":"MassTransit","description":"Deliver RCommon events across service boundaries via MassTransit consumers, with fan-out publish or point-to-point send and transport-agnostic ISubscriber handlers.","sidebar":"docsSidebar"},"event-handling/mediatr":{"id":"event-handling/mediatr","title":"MediatR","description":"Route RCommon events through MediatR's notification pipeline using MediatREventHandlingBuilder, with ISubscriber handlers unaware of MediatR internals.","sidebar":"docsSidebar"},"event-handling/overview":{"id":"event-handling/overview","title":"Overview","description":"Understand RCommon's unified event handling pipeline — IEventBus, IEventProducer, IEventRouter, and ISubscriber — and how to choose between in-process and broker transports.","sidebar":"docsSidebar"},"event-handling/transactional-outbox":{"id":"event-handling/transactional-outbox","title":"Transactional Outbox","description":"Guarantee at-least-once event delivery using the transactional outbox pattern with MassTransit EF Core outbox or Wolverine's EF Core transaction integration.","sidebar":"docsSidebar"},"event-handling/wolverine":{"id":"event-handling/wolverine","title":"Wolverine","description":"Use Wolverine as an event transport in RCommon's event handling pipeline, registering ISubscriber handlers and choosing between fan-out publish or point-to-point send.","sidebar":"docsSidebar"},"examples-recipes/caching":{"id":"examples-recipes/caching","title":"Caching Examples","description":"Practical RCommon caching examples covering IMemoryCache, distributed memory cache, Redis, and ICachingGraphRepository for caching EF Core repository query results.","sidebar":"docsSidebar"},"examples-recipes/event-handling":{"id":"examples-recipes/event-handling","title":"Event Handling Examples","description":"Step-by-step event handling examples for RCommon using InMemoryEventBus and MediatR providers with ISyncEvent, IEventProducer, and ISubscriber patterns.","sidebar":"docsSidebar"},"examples-recipes/hr-leave-management":{"id":"examples-recipes/hr-leave-management","title":"HR Leave Management Sample","description":"Explore RCommon's HR Leave Management reference app demonstrating Clean Architecture, CQRS, EF Core, FluentValidation, JWT auth, and SendGrid email integration.","sidebar":"docsSidebar"},"examples-recipes/messaging":{"id":"examples-recipes/messaging","title":"Messaging Examples","description":"Messaging examples for RCommon showing MassTransit and Wolverine transports, subscription isolation between multiple builders, and in-process vs distributed routing.","sidebar":"docsSidebar"},"getting-started/configuration":{"id":"getting-started/configuration","title":"Configuration & Bootstrapping","description":"Configure RCommon with AddRCommon() — fluent builder methods for GUID generation, system time, persistence, unit of work, mediator, and event handling.","sidebar":"docsSidebar"},"getting-started/dependency-injection":{"id":"getting-started/dependency-injection","title":"Dependency Injection","description":"How RCommon integrates with Microsoft.Extensions.DependencyInjection — service lifetimes, injecting repositories, IEventBus, ISystemTime, and named data stores.","sidebar":"docsSidebar"},"getting-started/installation":{"id":"getting-started/installation","title":"Installation","description":"Install RCommon NuGet packages for .NET 8, 9, or 10. Pick only what you need — persistence, CQRS, messaging, caching, validation, blob storage, and more.","sidebar":"docsSidebar"},"getting-started/overview":{"id":"getting-started/overview","title":"Overview & Philosophy","description":"Learn what RCommon provides — pluggable .NET abstractions for persistence, CQRS, domain events, messaging, and caching with a fluent DI-first configuration API.","sidebar":"docsSidebar"},"getting-started/quick-start":{"id":"getting-started/quick-start","title":"Quick Start Guide","description":"Build a .NET web API with RCommon in minutes — define entities, configure EF Core persistence, and query via ILinqRepository using a single fluent builder chain.","sidebar":"docsSidebar"},"index":{"id":"index","title":"RCommon Documentation","description":"Complete documentation for RCommon — a .NET infrastructure library with persistence, CQRS, event handling, messaging, caching, and validation abstractions."},"messaging/masstransit":{"id":"messaging/masstransit","title":"MassTransit","description":"Configure MassTransit as RCommon's messaging transport, register consumers and producers, choose RabbitMQ or Azure Service Bus, and keep handler code broker-free.","sidebar":"docsSidebar"},"messaging/overview":{"id":"messaging/overview","title":"Overview","description":"Overview of RCommon's broker-backed messaging layer built on IEventRouter and IEventProducer, comparing MassTransit and Wolverine transports for cross-service delivery.","sidebar":"docsSidebar"},"messaging/state-machines":{"id":"messaging/state-machines","title":"State Machines","description":"Orchestrate long-running workflows in RCommon using MassTransit's dictionary-based state machine adapter with IStateMachineConfigurator, guards, and async entry/exit actions.","sidebar":"docsSidebar"},"messaging/transactional-outbox":{"id":"messaging/transactional-outbox","title":"Transactional Outbox","description":"Ensure messages reach the broker only when the database transaction commits using the transactional outbox for MassTransit or Wolverine with EF Core integration.","sidebar":"docsSidebar"},"messaging/wolverine":{"id":"messaging/wolverine","title":"Wolverine","description":"Configure Wolverine as RCommon's messaging transport with durable delivery, fan-out publish or point-to-point send, and ISubscriber handlers free of Wolverine dependencies.","sidebar":"docsSidebar"},"multi-tenancy/finbuckle":{"id":"multi-tenancy/finbuckle","title":"Finbuckle","description":"RCommon.Finbuckle integrates Finbuckle.MultiTenant with RCommon's persistence layer, automatically scoping queries and entity writes to the request-resolved tenant.","sidebar":"docsSidebar"},"multi-tenancy/overview":{"id":"multi-tenancy/overview","title":"Overview","description":"Multi-tenancy abstraction for RCommon that automatically scopes repository queries and entity stamps to the active tenant via ITenantIdAccessor and IMultiTenant.","sidebar":"docsSidebar"},"persistence/caching-memory":{"id":"persistence/caching-memory","title":"Caching with Memory","description":"Decorate RCommon repositories with in-process memory caching using ICachingGraphRepository and caller-supplied cache keys, with write operations always bypassing the cache.","sidebar":"docsSidebar"},"persistence/caching-redis":{"id":"persistence/caching-redis","title":"Caching with Redis","description":"Back RCommon's repository caching decorators with Redis via RedisCacheService for a distributed, out-of-process cache shared across multiple service instances and restarts.","sidebar":"docsSidebar"},"persistence/dapper":{"id":"persistence/dapper","title":"Dapper","description":"Use RCommon's Dapper provider for lightweight SQL data access with Dommel-generated queries, raw SQL via ISqlMapperRepository, and automatic soft-delete handling.","sidebar":"docsSidebar"},"persistence/efcore":{"id":"persistence/efcore","title":"Entity Framework Core","description":"Configure RCommon's EF Core provider to register named DbContexts, inject IGraphRepository with full LINQ support, eager loading, soft delete, and change-tracking control.","sidebar":"docsSidebar"},"persistence/linq2db":{"id":"persistence/linq2db","title":"Linq2Db","description":"Wire up RCommon's Linq2Db provider for ILinqRepository support with LINQ queries, paging, eager loading via LoadWith, and multi-database connection management without EF Core.","sidebar":"docsSidebar"},"persistence/repository-pattern":{"id":"persistence/repository-pattern","title":"Repository Pattern","description":"Use RCommon's provider-agnostic repository interfaces for async CRUD, paging, eager loading, and soft delete across EF Core, Dapper, and Linq2Db without coupling to a specific ORM.","sidebar":"docsSidebar"},"persistence/sagas":{"id":"persistence/sagas","title":"Sagas","description":"Orchestrate long-running business processes with RCommon's saga framework using state machines, durable SagaState persistence, correlation IDs, and automatic compensation.","sidebar":"docsSidebar"},"persistence/specifications":{"id":"persistence/specifications","title":"Specifications","description":"Encapsulate reusable query predicates as named Specification objects, compose them with & and | operators, and pass them directly to any RCommon repository method.","sidebar":"docsSidebar"},"persistence/unit-of-work":{"id":"persistence/unit-of-work","title":"Unit of Work","description":"Coordinate writes across multiple repositories in a single TransactionScope using RCommon's IUnitOfWorkFactory, with MediatR and Wolverine pipeline integration support.","sidebar":"docsSidebar"},"security-web/authorization":{"id":"security-web/authorization","title":"Authorization","description":"RCommon.Security provides ICurrentUser and ICurrentPrincipalAccessor for claims-based identity resolution, plus Swashbuckle filters for OpenAPI authorization docs.","sidebar":"docsSidebar"},"security-web/web-utilities":{"id":"security-web/web-utilities","title":"Web Utilities","description":"RCommon.Web bridges ASP.NET Core's HttpContext.User to ICurrentPrincipalAccessor, replacing the thread-based default for correct identity resolution in web apps.","sidebar":"docsSidebar"},"serialization/newtonsoft":{"id":"serialization/newtonsoft","title":"Newtonsoft.Json","description":"RCommon.JsonNet wraps Newtonsoft.Json behind IJsonSerializer with full JsonSerializerSettings access, custom converters, and camelCase or indented output options.","sidebar":"docsSidebar"},"serialization/overview":{"id":"serialization/overview","title":"Overview","description":"Provider-agnostic JSON serialization for .NET with IJsonSerializer, supporting Newtonsoft.Json and System.Text.Json with per-call and global option configuration.","sidebar":"docsSidebar"},"serialization/system-text-json":{"id":"serialization/system-text-json","title":"System.Text.Json","description":"RCommon.SystemTextJson wraps System.Text.Json behind IJsonSerializer with full JsonSerializerOptions access and built-in integer and byte enum converters.","sidebar":"docsSidebar"},"state-machines/overview":{"id":"state-machines/overview","title":"Overview","description":"Provider-agnostic finite state machine abstraction for .NET using C# enums, with Stateless and MassTransit adapter options for workflow and lifecycle modeling.","sidebar":"docsSidebar"},"state-machines/stateless":{"id":"state-machines/stateless","title":"Stateless","description":"RCommon.Stateless wraps the Stateless library behind IStateMachineConfigurator, supporting guarded transitions, parameterized triggers, and async entry/exit actions.","sidebar":"docsSidebar"},"testing/overview":{"id":"testing/overview","title":"Overview","description":"Learn how to test .NET applications built on RCommon using mocks, EF Core InMemory, and xUnit or NUnit base classes for unit and integration test strategies.","sidebar":"docsSidebar"},"testing/test-base-classes":{"id":"testing/test-base-classes","title":"Test Base Classes","description":"Reference guide for RCommon.TestBase, TestBootstrapper, TestFixture for xUnit, TestRepository for EF Core integration tests, and TestDataActions fake data generation.","sidebar":"docsSidebar"},"validation/fluent-validation":{"id":"validation/fluent-validation","title":"FluentValidation","description":"RCommon.FluentValidation integrates FluentValidation with the CQRS pipeline for automatic command and query validation, with manual IValidationService support.","sidebar":"docsSidebar"}}}} \ No newline at end of file diff --git a/website/.docusaurus/docusaurus-plugin-content-docs/default/site-docs-core-concepts-fluent-configuration-mdx-b19.json b/website/.docusaurus/docusaurus-plugin-content-docs/default/site-docs-core-concepts-fluent-configuration-mdx-b19.json index a9ee333e..4e90f60a 100644 --- a/website/.docusaurus/docusaurus-plugin-content-docs/default/site-docs-core-concepts-fluent-configuration-mdx-b19.json +++ b/website/.docusaurus/docusaurus-plugin-content-docs/default/site-docs-core-concepts-fluent-configuration-mdx-b19.json @@ -23,7 +23,7 @@ "permalink": "/docs/next/category/core-concepts" }, "next": { - "title": "Guards & Validation", - "permalink": "/docs/next/core-concepts/guards" + "title": "Modular Composition", + "permalink": "/docs/next/core-concepts/modular-composition" } } \ No newline at end of file diff --git a/website/.docusaurus/docusaurus-plugin-content-docs/default/site-docs-core-concepts-guards-mdx-d7b.json b/website/.docusaurus/docusaurus-plugin-content-docs/default/site-docs-core-concepts-guards-mdx-d7b.json index 48678e34..57978598 100644 --- a/website/.docusaurus/docusaurus-plugin-content-docs/default/site-docs-core-concepts-guards-mdx-d7b.json +++ b/website/.docusaurus/docusaurus-plugin-content-docs/default/site-docs-core-concepts-guards-mdx-d7b.json @@ -19,8 +19,8 @@ }, "sidebar": "docsSidebar", "previous": { - "title": "Fluent Configuration", - "permalink": "/docs/next/core-concepts/fluent-configuration" + "title": "Modular Composition", + "permalink": "/docs/next/core-concepts/modular-composition" }, "next": { "title": "GUID Generation", diff --git a/website/.docusaurus/docusaurus-plugin-content-docs/default/site-docs-core-concepts-modular-composition-mdx-e3b.json b/website/.docusaurus/docusaurus-plugin-content-docs/default/site-docs-core-concepts-modular-composition-mdx-e3b.json new file mode 100644 index 00000000..a518dfeb --- /dev/null +++ b/website/.docusaurus/docusaurus-plugin-content-docs/default/site-docs-core-concepts-modular-composition-mdx-e3b.json @@ -0,0 +1,29 @@ +{ + "id": "core-concepts/modular-composition", + "title": "Modular Composition", + "description": "Compose AddRCommon across multiple modules in a single process — registrations merge, never duplicate or throw on agreement.", + "source": "@site/docs/core-concepts/modular-composition.mdx", + "sourceDirName": "core-concepts", + "slug": "/core-concepts/modular-composition", + "permalink": "/docs/next/core-concepts/modular-composition", + "draft": false, + "unlisted": false, + "editUrl": "https://github.com/RCommon-Team/RCommon/tree/main/website/docs/core-concepts/modular-composition.mdx", + "tags": [], + "version": "current", + "sidebarPosition": 2, + "frontMatter": { + "title": "Modular Composition", + "sidebar_position": 2, + "description": "Compose AddRCommon across multiple modules in a single process — registrations merge, never duplicate or throw on agreement." + }, + "sidebar": "docsSidebar", + "previous": { + "title": "Fluent Configuration", + "permalink": "/docs/next/core-concepts/fluent-configuration" + }, + "next": { + "title": "Guards & Validation", + "permalink": "/docs/next/core-concepts/guards" + } +} \ No newline at end of file diff --git a/website/.docusaurus/docusaurus.config.mjs b/website/.docusaurus/docusaurus.config.mjs index a124c0bb..5af090c9 100644 --- a/website/.docusaurus/docusaurus.config.mjs +++ b/website/.docusaurus/docusaurus.config.mjs @@ -22,6 +22,13 @@ export default { "path": "i18n", "localeConfigs": {} }, + "scripts": [ + { + "src": "https://context7.com/widget.js", + "data-library": "/rcommon-team/rcommon", + "async": true + } + ], "headTags": [ { "tagName": "meta", @@ -416,7 +423,6 @@ export default { "static" ], "customFields": {}, - "scripts": [], "stylesheets": [], "clientModules": [], "titleDelimiter": "|", diff --git a/website/.docusaurus/globalData.json b/website/.docusaurus/globalData.json index 5685306d..99de64bf 100644 --- a/website/.docusaurus/globalData.json +++ b/website/.docusaurus/globalData.json @@ -90,6 +90,11 @@ "path": "/docs/next/core-concepts/guid-generation", "sidebar": "docsSidebar" }, + { + "id": "core-concepts/modular-composition", + "path": "/docs/next/core-concepts/modular-composition", + "sidebar": "docsSidebar" + }, { "id": "core-concepts/system-time", "path": "/docs/next/core-concepts/system-time", diff --git a/website/.docusaurus/registry.js b/website/.docusaurus/registry.js index 480752d0..b1b7afab 100644 --- a/website/.docusaurus/registry.js +++ b/website/.docusaurus/registry.js @@ -166,6 +166,7 @@ export default { "e1418cd9": [() => import(/* webpackChunkName: "e1418cd9" */ "@generated/docusaurus-plugin-content-docs/default/p/docs-category-event-handling-57b.json"), "@generated/docusaurus-plugin-content-docs/default/p/docs-category-event-handling-57b.json", require.resolveWeak("@generated/docusaurus-plugin-content-docs/default/p/docs-category-event-handling-57b.json")], "e1da346e": [() => import(/* webpackChunkName: "e1da346e" */ "@site/versioned_docs/version-2.4.1/cqrs-mediator/command-query-bus.mdx"), "@site/versioned_docs/version-2.4.1/cqrs-mediator/command-query-bus.mdx", require.resolveWeak("@site/versioned_docs/version-2.4.1/cqrs-mediator/command-query-bus.mdx")], "e3b543e0": [() => import(/* webpackChunkName: "e3b543e0" */ "@site/versioned_docs/version-2.4.1/architecture-guides/microservices.mdx"), "@site/versioned_docs/version-2.4.1/architecture-guides/microservices.mdx", require.resolveWeak("@site/versioned_docs/version-2.4.1/architecture-guides/microservices.mdx")], + "e3ba908e": [() => import(/* webpackChunkName: "e3ba908e" */ "@site/docs/core-concepts/modular-composition.mdx"), "@site/docs/core-concepts/modular-composition.mdx", require.resolveWeak("@site/docs/core-concepts/modular-composition.mdx")], "e3d4b007": [() => import(/* webpackChunkName: "e3d4b007" */ "@generated/docusaurus-plugin-content-docs/default/p/docs-category-caching-248.json"), "@generated/docusaurus-plugin-content-docs/default/p/docs-category-caching-248.json", require.resolveWeak("@generated/docusaurus-plugin-content-docs/default/p/docs-category-caching-248.json")], "e592b2d8": [() => import(/* webpackChunkName: "e592b2d8" */ "@site/docs/domain-driven-design/domain-events.mdx"), "@site/docs/domain-driven-design/domain-events.mdx", require.resolveWeak("@site/docs/domain-driven-design/domain-events.mdx")], "e59bb016": [() => import(/* webpackChunkName: "e59bb016" */ "@site/docs/core-concepts/system-time.mdx"), "@site/docs/core-concepts/system-time.mdx", require.resolveWeak("@site/docs/core-concepts/system-time.mdx")], diff --git a/website/.docusaurus/routes.js b/website/.docusaurus/routes.js index 23c1381c..e3aa7304 100644 --- a/website/.docusaurus/routes.js +++ b/website/.docusaurus/routes.js @@ -9,15 +9,15 @@ export default [ }, { path: '/docs', - component: ComponentCreator('/docs', '62c'), + component: ComponentCreator('/docs', '732'), routes: [ { path: '/docs/next', - component: ComponentCreator('/docs/next', 'f3e'), + component: ComponentCreator('/docs/next', '1a8'), routes: [ { path: '/docs/next', - component: ComponentCreator('/docs/next', 'a23'), + component: ComponentCreator('/docs/next', 'bf6'), routes: [ { path: '/docs/next', @@ -234,6 +234,12 @@ export default [ exact: true, sidebar: "docsSidebar" }, + { + path: '/docs/next/core-concepts/modular-composition', + component: ComponentCreator('/docs/next/core-concepts/modular-composition', '40a'), + exact: true, + sidebar: "docsSidebar" + }, { path: '/docs/next/core-concepts/system-time', component: ComponentCreator('/docs/next/core-concepts/system-time', '1a0'), diff --git a/website/.docusaurus/routesChunkNames.json b/website/.docusaurus/routesChunkNames.json index 1ba1647a..cc0a73d4 100644 --- a/website/.docusaurus/routesChunkNames.json +++ b/website/.docusaurus/routesChunkNames.json @@ -5,17 +5,17 @@ "plugin": "138e0e15" } }, - "/docs-62c": { + "/docs-732": { "__comp": "5e95c892", "__context": { "plugin": "aba21aa0" } }, - "/docs/next-f3e": { + "/docs/next-1a8": { "__comp": "a7bd4aaa", "__props": "bf1307fc" }, - "/docs/next-a23": { + "/docs/next-bf6": { "__comp": "a94703ab" }, "/docs/next-c40": { @@ -162,6 +162,10 @@ "__comp": "17896441", "content": "217dc876" }, + "/docs/next/core-concepts/modular-composition-40a": { + "__comp": "17896441", + "content": "e3ba908e" + }, "/docs/next/core-concepts/system-time-1a0": { "__comp": "17896441", "content": "e59bb016" diff --git a/website/build/404.html b/website/build/404.html index a259d0d9..0cc7f13a 100644 --- a/website/build/404.html +++ b/website/build/404.html @@ -13,9 +13,10 @@ - - - + + + +

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

RCommonRCommon
diff --git a/website/build/assets/js/0190ef14.618325c7.js b/website/build/assets/js/0190ef14.618325c7.js deleted file mode 100644 index f18aae60..00000000 --- a/website/build/assets/js/0190ef14.618325c7.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[8995],{50478(e,n,r){r.r(n),r.d(n,{assets:()=>c,contentTitle:()=>d,default:()=>u,frontMatter:()=>a,metadata:()=>o,toc:()=>l});var s=r(86070),t=r(81753),i=r(75783);const a={title:"MassTransit",sidebar_position:6,description:"Deliver RCommon events across service boundaries via MassTransit consumers, with fan-out publish or point-to-point send and transport-agnostic ISubscriber handlers."},d="MassTransit",o={id:"event-handling/masstransit",title:"MassTransit",description:"Deliver RCommon events across service boundaries via MassTransit consumers, with fan-out publish or point-to-point send and transport-agnostic ISubscriber handlers.",source:"@site/docs/event-handling/masstransit.mdx",sourceDirName:"event-handling",slug:"/event-handling/masstransit",permalink:"/docs/next/event-handling/masstransit",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/event-handling/masstransit.mdx",tags:[],version:"current",sidebarPosition:6,frontMatter:{title:"MassTransit",sidebar_position:6,description:"Deliver RCommon events across service boundaries via MassTransit consumers, with fan-out publish or point-to-point send and transport-agnostic ISubscriber handlers."},sidebar:"docsSidebar",previous:{title:"MediatR",permalink:"/docs/next/event-handling/mediatr"},next:{title:"Wolverine",permalink:"/docs/next/event-handling/wolverine"}},c={},l=[{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Defining an event",id:"defining-an-event",level:2},{value:"Implementing a subscriber",id:"implementing-a-subscriber",level:2},{value:"Publishing events",id:"publishing-events",level:2},{value:"Publish vs Send",id:"publish-vs-send",level:2},{value:"Transactional outbox",id:"transactional-outbox",level:2},{value:"How the consumer bridge works",id:"how-the-consumer-bridge-works",level:2},{value:"API summary",id:"api-summary",level:2}];function h(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"masstransit",children:"MassTransit"}),"\n",(0,s.jsxs)(n.p,{children:["RCommon integrates with MassTransit to deliver events to consumers across service boundaries. The integration uses ",(0,s.jsx)(n.code,{children:"MassTransitEventHandler"})," as a MassTransit ",(0,s.jsx)(n.code,{children:"IConsumer"})," that delegates to the application's ",(0,s.jsx)(n.code,{children:"ISubscriber"})," implementation. Application handler code has no dependency on MassTransit types."]}),"\n",(0,s.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,s.jsx)(i.A,{packageName:"RCommon.MassTransit"}),"\n",(0,s.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,s.jsxs)(n.p,{children:["Use ",(0,s.jsx)(n.code,{children:"WithEventHandling"})," on the RCommon builder. The builder inherits from MassTransit's ",(0,s.jsx)(n.code,{children:"ServiceCollectionBusConfigurator"}),", so all standard MassTransit configuration APIs are available directly on it:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'using RCommon;\r\nusing RCommon.MassTransit;\r\nusing RCommon.MassTransit.Producers;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithEventHandling(mt =>\r\n {\r\n // Register the producer that publishes events to the broker\r\n mt.AddProducer();\r\n\r\n // Register subscribers; each call also adds a MassTransit consumer\r\n mt.AddSubscriber();\r\n mt.AddSubscriber();\r\n\r\n // Standard MassTransit transport configuration\r\n mt.UsingRabbitMq((ctx, cfg) =>\r\n {\r\n cfg.Host("rabbitmq://localhost");\r\n cfg.ConfigureEndpoints(ctx);\r\n });\r\n });\n'})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"AddSubscriber"})," performs three registrations:"]}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["Registers ",(0,s.jsx)(n.code,{children:"ISubscriber"})," in the DI container as transient."]}),"\n",(0,s.jsxs)(n.li,{children:["Calls ",(0,s.jsx)(n.code,{children:"AddConsumer>"})," so MassTransit creates a consumer for the endpoint."]}),"\n",(0,s.jsxs)(n.li,{children:["Records the event-to-producer association in the ",(0,s.jsx)(n.code,{children:"EventSubscriptionManager"})," so the router routes this event only to the MassTransit producer."]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"defining-an-event",children:"Defining an event"}),"\n",(0,s.jsxs)(n.p,{children:["Events must implement ",(0,s.jsx)(n.code,{children:"ISerializableEvent"})," and must have a parameterless constructor for broker deserialization:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Models.Events;\r\n\r\npublic class OrderShipped : ISyncEvent\r\n{\r\n public OrderShipped(Guid orderId, string trackingNumber)\r\n {\r\n OrderId = orderId;\r\n TrackingNumber = trackingNumber;\r\n }\r\n\r\n public OrderShipped() { }\r\n\r\n public Guid OrderId { get; }\r\n public string TrackingNumber { get; } = string.Empty;\r\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"implementing-a-subscriber",children:"Implementing a subscriber"}),"\n",(0,s.jsxs)(n.p,{children:["Implement ",(0,s.jsx)(n.code,{children:"ISubscriber"}),". No MassTransit types appear in handler code:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'using RCommon.EventHandling.Subscribers;\r\n\r\npublic class OrderShippedHandler : ISubscriber\r\n{\r\n private readonly ILogger _logger;\r\n\r\n public OrderShippedHandler(ILogger logger)\r\n {\r\n _logger = logger;\r\n }\r\n\r\n public async Task HandleAsync(OrderShipped @event, CancellationToken cancellationToken = default)\r\n {\r\n _logger.LogInformation(\r\n "Order {OrderId} shipped with tracking {TrackingNumber}",\r\n @event.OrderId, @event.TrackingNumber);\r\n await Task.CompletedTask;\r\n }\r\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"publishing-events",children:"Publishing events"}),"\n",(0,s.jsxs)(n.p,{children:["Call ",(0,s.jsx)(n.code,{children:"IEventRouter.RouteEventsAsync"})," after adding transactional events. The router forwards each event to ",(0,s.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"}),", which calls ",(0,s.jsx)(n.code,{children:"IBus.Publish"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public class ShippingService\r\n{\r\n private readonly IEventRouter _eventRouter;\r\n\r\n public ShippingService(IEventRouter eventRouter)\r\n {\r\n _eventRouter = eventRouter;\r\n }\r\n\r\n public async Task ShipOrderAsync(Guid orderId, string trackingNumber, CancellationToken cancellationToken)\r\n {\r\n // ... update order state ...\r\n\r\n _eventRouter.AddTransactionalEvent(new OrderShipped(orderId, trackingNumber));\r\n await _eventRouter.RouteEventsAsync(cancellationToken);\r\n }\r\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"publish-vs-send",children:"Publish vs Send"}),"\n",(0,s.jsxs)(n.p,{children:["Register ",(0,s.jsx)(n.code,{children:"SendWithMassTransitEventProducer"})," instead of (or in addition to) ",(0,s.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})," when you want point-to-point delivery to a single consumer endpoint:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"mt.AddProducer();\n"})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"SendWithMassTransitEventProducer"})," calls ",(0,s.jsx)(n.code,{children:"IBus.Send"})," internally. It is appropriate for command-style messages where only one consumer should process the event."]}),"\n",(0,s.jsx)(n.h2,{id:"transactional-outbox",children:"Transactional outbox"}),"\n",(0,s.jsxs)(n.p,{children:["Pair MassTransit with the outbox pattern to guarantee at-least-once delivery. See ",(0,s.jsx)(n.a,{href:"/docs/next/event-handling/transactional-outbox#masstransit-outbox",children:"Transactional Outbox"}),"."]}),"\n",(0,s.jsx)(n.h2,{id:"how-the-consumer-bridge-works",children:"How the consumer bridge works"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"MassTransitEventHandler"})," implements both ",(0,s.jsx)(n.code,{children:"IMassTransitEventHandler"})," and MassTransit's ",(0,s.jsx)(n.code,{children:"IConsumer"}),". When MassTransit delivers a message, it calls ",(0,s.jsx)(n.code,{children:"Consume(ConsumeContext)"}),", which resolves ",(0,s.jsx)(n.code,{children:"ISubscriber"})," from DI and calls ",(0,s.jsx)(n.code,{children:"HandleAsync"}),". This keeps application handler code free of any MassTransit API surface."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"// Framework code \u2014 you do not write this yourself\r\npublic class MassTransitEventHandler : IConsumer\r\n where TEvent : class, ISerializableEvent\r\n{\r\n public async Task Consume(ConsumeContext context)\r\n {\r\n await _subscriber.HandleAsync(context.Message, context.CancellationToken);\r\n }\r\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"api-summary",children:"API summary"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"})}),(0,s.jsxs)(n.td,{children:["Builder used with ",(0,s.jsx)(n.code,{children:"WithEventHandling"}),"; inherits ",(0,s.jsx)(n.code,{children:"ServiceCollectionBusConfigurator"})]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"IMassTransitEventHandlingBuilder"})}),(0,s.jsx)(n.td,{children:"Interface for the MassTransit event handling builder"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})}),(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"IEventProducer"})," that calls ",(0,s.jsx)(n.code,{children:"IBus.Publish"})," (fan-out)"]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"SendWithMassTransitEventProducer"})}),(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"IEventProducer"})," that calls ",(0,s.jsx)(n.code,{children:"IBus.Send"})," (point-to-point)"]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MassTransitEventHandler"})}),(0,s.jsxs)(n.td,{children:["Internal ",(0,s.jsx)(n.code,{children:"IConsumer"})," that bridges MassTransit to ",(0,s.jsx)(n.code,{children:"ISubscriber"})]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"IMassTransitEventHandler"})}),(0,s.jsx)(n.td,{children:"Marker interface for MassTransit event handlers"})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}},75783(e,n,r){r.d(n,{A:()=>l});var s=r(30758);const t="container_xjrG",i="label_Y4p8",a="commandRow_FY5I",d="command_m7Qs",o="copyButton_u1GK";var c=r(86070);function l({packageName:e,version:n}){const[r,l]=(0,s.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,c.jsxs)("div",{className:t,children:[(0,c.jsx)("div",{className:i,children:"NuGet Package"}),(0,c.jsxs)("div",{className:a,children:[(0,c.jsx)("code",{className:d,children:h}),(0,c.jsx)("button",{className:o,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:r?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,r){r.d(n,{R:()=>a,x:()=>d});var s=r(30758);const t={},i=s.createContext(t);function a(e){const n=s.useContext(i);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:a(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/0190ef14.72dca7bf.js b/website/build/assets/js/0190ef14.72dca7bf.js new file mode 100644 index 00000000..5142c684 --- /dev/null +++ b/website/build/assets/js/0190ef14.72dca7bf.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[8995],{50478(e,n,s){s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>d,default:()=>u,frontMatter:()=>a,metadata:()=>o,toc:()=>l});var r=s(86070),i=s(81753),t=s(75783);const a={title:"MassTransit",sidebar_position:6,description:"Deliver RCommon events across service boundaries via MassTransit consumers, with fan-out publish or point-to-point send and transport-agnostic ISubscriber handlers."},d="MassTransit",o={id:"event-handling/masstransit",title:"MassTransit",description:"Deliver RCommon events across service boundaries via MassTransit consumers, with fan-out publish or point-to-point send and transport-agnostic ISubscriber handlers.",source:"@site/docs/event-handling/masstransit.mdx",sourceDirName:"event-handling",slug:"/event-handling/masstransit",permalink:"/docs/next/event-handling/masstransit",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/event-handling/masstransit.mdx",tags:[],version:"current",sidebarPosition:6,frontMatter:{title:"MassTransit",sidebar_position:6,description:"Deliver RCommon events across service boundaries via MassTransit consumers, with fan-out publish or point-to-point send and transport-agnostic ISubscriber handlers."},sidebar:"docsSidebar",previous:{title:"MediatR",permalink:"/docs/next/event-handling/mediatr"},next:{title:"Wolverine",permalink:"/docs/next/event-handling/wolverine"}},c={},l=[{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Defining an event",id:"defining-an-event",level:2},{value:"Implementing a subscriber",id:"implementing-a-subscriber",level:2},{value:"Publishing events",id:"publishing-events",level:2},{value:"Publish vs Send",id:"publish-vs-send",level:2},{value:"Transactional outbox",id:"transactional-outbox",level:2},{value:"How the consumer bridge works",id:"how-the-consumer-bridge-works",level:2},{value:"API summary",id:"api-summary",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"masstransit",children:"MassTransit"}),"\n",(0,r.jsxs)(n.p,{children:["RCommon integrates with MassTransit to deliver events to consumers across service boundaries. The integration uses ",(0,r.jsx)(n.code,{children:"MassTransitEventHandler"})," as a MassTransit ",(0,r.jsx)(n.code,{children:"IConsumer"})," that delegates to the application's ",(0,r.jsx)(n.code,{children:"ISubscriber"})," implementation. Application handler code has no dependency on MassTransit types."]}),"\n",(0,r.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(t.A,{packageName:"RCommon.MassTransit"}),"\n",(0,r.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.code,{children:"WithEventHandling"})," on the RCommon builder. The builder inherits from MassTransit's ",(0,r.jsx)(n.code,{children:"ServiceCollectionBusConfigurator"}),", so all standard MassTransit configuration APIs are available directly on it:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon;\nusing RCommon.MassTransit;\nusing RCommon.MassTransit.Producers;\n\nbuilder.Services.AddRCommon()\n .WithEventHandling(mt =>\n {\n // Register the producer that publishes events to the broker\n mt.AddProducer();\n\n // Register subscribers; each call also adds a MassTransit consumer\n mt.AddSubscriber();\n mt.AddSubscriber();\n\n // Standard MassTransit transport configuration\n mt.UsingRabbitMq((ctx, cfg) =>\n {\n cfg.Host("rabbitmq://localhost");\n cfg.ConfigureEndpoints(ctx);\n });\n });\n'})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"AddSubscriber"})," performs three registrations:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["Registers ",(0,r.jsx)(n.code,{children:"ISubscriber"})," in the DI container as transient."]}),"\n",(0,r.jsxs)(n.li,{children:["Calls ",(0,r.jsx)(n.code,{children:"AddConsumer>"})," so MassTransit creates a consumer for the endpoint."]}),"\n",(0,r.jsxs)(n.li,{children:["Records the event-to-producer association in the ",(0,r.jsx)(n.code,{children:"EventSubscriptionManager"})," so the router routes this event only to the MassTransit producer."]}),"\n"]}),"\n",(0,r.jsxs)(n.admonition,{title:"Modular composition",type:"tip",children:[(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"WithEventHandling"})," participates in the cache-aware sub-builder contract: a second call with the same builder type reuses the cached ",(0,r.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"}),", and subscriber/producer registrations from each module accumulate against the one instance. ",(0,r.jsx)(n.code,{children:"AddProducer"})," deduplicates by concrete producer type \u2014 exactly one descriptor per producer."]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Known limitation \u2014 cross-module composition."})," MassTransit's pre-existing ",(0,r.jsx)(n.code,{children:"IBus"})," registration guard means a second module that calls ",(0,r.jsx)(n.code,{children:"WithEventHandling"})," will throw ",(0,r.jsx)(n.code,{children:"MassTransit.ConfigurationException"}),". Single-module usage is unchanged. Tracked as a follow-up; for now, funnel MassTransit wiring through a single module and let other modules contribute subscribers via the shared cached builder accessed through ",(0,r.jsx)(n.code,{children:"IRCommonBuilder.GetOrAddBuilder()"}),"."]}),(0,r.jsxs)(n.p,{children:["See ",(0,r.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})]}),"\n",(0,r.jsx)(n.h2,{id:"defining-an-event",children:"Defining an event"}),"\n",(0,r.jsxs)(n.p,{children:["Events must implement ",(0,r.jsx)(n.code,{children:"ISerializableEvent"})," and must have a parameterless constructor for broker deserialization:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Models.Events;\n\npublic class OrderShipped : ISyncEvent\n{\n public OrderShipped(Guid orderId, string trackingNumber)\n {\n OrderId = orderId;\n TrackingNumber = trackingNumber;\n }\n\n public OrderShipped() { }\n\n public Guid OrderId { get; }\n public string TrackingNumber { get; } = string.Empty;\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"implementing-a-subscriber",children:"Implementing a subscriber"}),"\n",(0,r.jsxs)(n.p,{children:["Implement ",(0,r.jsx)(n.code,{children:"ISubscriber"}),". No MassTransit types appear in handler code:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon.EventHandling.Subscribers;\n\npublic class OrderShippedHandler : ISubscriber\n{\n private readonly ILogger _logger;\n\n public OrderShippedHandler(ILogger logger)\n {\n _logger = logger;\n }\n\n public async Task HandleAsync(OrderShipped @event, CancellationToken cancellationToken = default)\n {\n _logger.LogInformation(\n "Order {OrderId} shipped with tracking {TrackingNumber}",\n @event.OrderId, @event.TrackingNumber);\n await Task.CompletedTask;\n }\n}\n'})}),"\n",(0,r.jsx)(n.h2,{id:"publishing-events",children:"Publishing events"}),"\n",(0,r.jsxs)(n.p,{children:["Call ",(0,r.jsx)(n.code,{children:"IEventRouter.RouteEventsAsync"})," after adding transactional events. The router forwards each event to ",(0,r.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"}),", which calls ",(0,r.jsx)(n.code,{children:"IBus.Publish"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"public class ShippingService\n{\n private readonly IEventRouter _eventRouter;\n\n public ShippingService(IEventRouter eventRouter)\n {\n _eventRouter = eventRouter;\n }\n\n public async Task ShipOrderAsync(Guid orderId, string trackingNumber, CancellationToken cancellationToken)\n {\n // ... update order state ...\n\n _eventRouter.AddTransactionalEvent(new OrderShipped(orderId, trackingNumber));\n await _eventRouter.RouteEventsAsync(cancellationToken);\n }\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"publish-vs-send",children:"Publish vs Send"}),"\n",(0,r.jsxs)(n.p,{children:["Register ",(0,r.jsx)(n.code,{children:"SendWithMassTransitEventProducer"})," instead of (or in addition to) ",(0,r.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})," when you want point-to-point delivery to a single consumer endpoint:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"mt.AddProducer();\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"SendWithMassTransitEventProducer"})," calls ",(0,r.jsx)(n.code,{children:"IBus.Send"})," internally. It is appropriate for command-style messages where only one consumer should process the event."]}),"\n",(0,r.jsx)(n.h2,{id:"transactional-outbox",children:"Transactional outbox"}),"\n",(0,r.jsxs)(n.p,{children:["Pair MassTransit with the outbox pattern to guarantee at-least-once delivery. See ",(0,r.jsx)(n.a,{href:"/docs/next/event-handling/transactional-outbox#masstransit-outbox",children:"Transactional Outbox"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"how-the-consumer-bridge-works",children:"How the consumer bridge works"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"MassTransitEventHandler"})," implements both ",(0,r.jsx)(n.code,{children:"IMassTransitEventHandler"})," and MassTransit's ",(0,r.jsx)(n.code,{children:"IConsumer"}),". When MassTransit delivers a message, it calls ",(0,r.jsx)(n.code,{children:"Consume(ConsumeContext)"}),", which resolves ",(0,r.jsx)(n.code,{children:"ISubscriber"})," from DI and calls ",(0,r.jsx)(n.code,{children:"HandleAsync"}),". This keeps application handler code free of any MassTransit API surface."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"// Framework code \u2014 you do not write this yourself\npublic class MassTransitEventHandler : IConsumer\n where TEvent : class, ISerializableEvent\n{\n public async Task Consume(ConsumeContext context)\n {\n await _subscriber.HandleAsync(context.Message, context.CancellationToken);\n }\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"api-summary",children:"API summary"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"})}),(0,r.jsxs)(n.td,{children:["Builder used with ",(0,r.jsx)(n.code,{children:"WithEventHandling"}),"; inherits ",(0,r.jsx)(n.code,{children:"ServiceCollectionBusConfigurator"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"IMassTransitEventHandlingBuilder"})}),(0,r.jsx)(n.td,{children:"Interface for the MassTransit event handling builder"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"IEventProducer"})," that calls ",(0,r.jsx)(n.code,{children:"IBus.Publish"})," (fan-out)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SendWithMassTransitEventProducer"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"IEventProducer"})," that calls ",(0,r.jsx)(n.code,{children:"IBus.Send"})," (point-to-point)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"MassTransitEventHandler"})}),(0,r.jsxs)(n.td,{children:["Internal ",(0,r.jsx)(n.code,{children:"IConsumer"})," that bridges MassTransit to ",(0,r.jsx)(n.code,{children:"ISubscriber"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"IMassTransitEventHandler"})}),(0,r.jsx)(n.td,{children:"Marker interface for MassTransit event handlers"})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},75783(e,n,s){s.d(n,{A:()=>l});var r=s(30758);const i="container_xjrG",t="label_Y4p8",a="commandRow_FY5I",d="command_m7Qs",o="copyButton_u1GK";var c=s(86070);function l({packageName:e,version:n}){const[s,l]=(0,r.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,c.jsxs)("div",{className:i,children:[(0,c.jsx)("div",{className:t,children:"NuGet Package"}),(0,c.jsxs)("div",{className:a,children:[(0,c.jsx)("code",{className:d,children:h}),(0,c.jsx)("button",{className:o,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:s?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,s){s.d(n,{R:()=>a,x:()=>d});var r=s(30758);const i={},t=r.createContext(i);function a(e){const n=r.useContext(t);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),r.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/053d6f47.0d6e4e5f.js b/website/build/assets/js/053d6f47.0d6e4e5f.js new file mode 100644 index 00000000..271c4f31 --- /dev/null +++ b/website/build/assets/js/053d6f47.0d6e4e5f.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[7768],{81993(e,n,t){t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>a,default:()=>p,frontMatter:()=>s,metadata:()=>c,toc:()=>l});var i=t(86070),o=t(81753),r=t(75783);const s={title:"Quick Start Guide",sidebar_position:3,description:"Build a .NET web API with RCommon in minutes \u2014 define entities, configure EF Core persistence, and query via ILinqRepository using a single fluent builder chain."},a="Quick Start Guide",c={id:"getting-started/quick-start",title:"Quick Start Guide",description:"Build a .NET web API with RCommon in minutes \u2014 define entities, configure EF Core persistence, and query via ILinqRepository using a single fluent builder chain.",source:"@site/docs/getting-started/quick-start.mdx",sourceDirName:"getting-started",slug:"/getting-started/quick-start",permalink:"/docs/next/getting-started/quick-start",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/getting-started/quick-start.mdx",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"Quick Start Guide",sidebar_position:3,description:"Build a .NET web API with RCommon in minutes \u2014 define entities, configure EF Core persistence, and query via ILinqRepository using a single fluent builder chain."},sidebar:"docsSidebar",previous:{title:"Installation",permalink:"/docs/next/getting-started/installation"},next:{title:"Configuration & Bootstrapping",permalink:"/docs/next/getting-started/configuration"}},d={},l=[{value:"1. Create a new project",id:"1-create-a-new-project",level:2},{value:"2. Install packages",id:"2-install-packages",level:2},{value:"3. Define your entity",id:"3-define-your-entity",level:2},{value:"4. Create a DbContext",id:"4-create-a-dbcontext",level:2},{value:"5. Configure RCommon in Program.cs",id:"5-configure-rcommon-in-programcs",level:2},{value:"6. Inject and use the repository",id:"6-inject-and-use-the-repository",level:2},{value:"What happens under the hood",id:"what-happens-under-the-hood",level:2}];function u(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",ul:"ul",...(0,o.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"quick-start-guide",children:"Quick Start Guide"}),"\n",(0,i.jsx)(n.p,{children:"This guide walks through the minimum steps to create a new .NET web API, add RCommon, and run a repository query against a SQL Server database using Entity Framework Core."}),"\n",(0,i.jsx)(n.h2,{id:"1-create-a-new-project",children:"1. Create a new project"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"dotnet new webapi -n MyApp\ncd MyApp\n"})}),"\n",(0,i.jsx)(n.h2,{id:"2-install-packages",children:"2. Install packages"}),"\n",(0,i.jsx)(r.A,{packageName:"RCommon.Core"}),"\n",(0,i.jsx)(r.A,{packageName:"RCommon.Entities"}),"\n",(0,i.jsx)(r.A,{packageName:"RCommon.Persistence"}),"\n",(0,i.jsx)(r.A,{packageName:"RCommon.EFCore"}),"\n",(0,i.jsx)(n.h2,{id:"3-define-your-entity",children:"3. Define your entity"}),"\n",(0,i.jsxs)(n.p,{children:["Entities in RCommon inherit from a base class provided by ",(0,i.jsx)(n.code,{children:"RCommon.Entities"}),". The base classes handle common concerns like auditing and domain events automatically."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Entities;\n\npublic class Product : BusinessEntity\n{\n public string Name { get; set; } = string.Empty;\n public decimal Price { get; set; }\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"4-create-a-dbcontext",children:"4. Create a DbContext"}),"\n",(0,i.jsxs)(n.p,{children:["The ",(0,i.jsx)(n.code,{children:"RCommonDbContext"})," base class integrates with RCommon's change tracking and domain event dispatch."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using Microsoft.EntityFrameworkCore;\nusing RCommon.Persistence.EFCore;\n\npublic class AppDbContext : RCommonDbContext\n{\n public AppDbContext(DbContextOptions options,\n IEntityEventTracker eventTracker)\n : base(options, eventTracker)\n {\n }\n\n public DbSet Products => Set();\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"5-configure-rcommon-in-programcs",children:"5. Configure RCommon in Program.cs"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'using RCommon;\nusing RCommon.Persistence.EFCore;\nusing RCommon.Persistence.Transactions;\nusing Microsoft.EntityFrameworkCore;\nusing System.Transactions;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddRCommon()\n .WithSequentialGuidGenerator(guid =>\n guid.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString)\n .WithDateTimeSystem(dt => dt.Kind = DateTimeKind.Utc)\n .WithPersistence(ef =>\n {\n ef.AddDbContext("AppDb", options =>\n {\n options.UseSqlServer(\n builder.Configuration.GetConnectionString("DefaultConnection"));\n });\n ef.SetDefaultDataStore(ds =>\n {\n ds.DefaultDataStoreName = "AppDb";\n });\n })\n .WithUnitOfWork(uow =>\n {\n uow.SetOptions(options =>\n {\n options.AutoCompleteScope = true;\n options.DefaultIsolation = IsolationLevel.ReadCommitted;\n });\n });\n\nbuilder.Services.AddControllers();\n\nvar app = builder.Build();\napp.MapControllers();\napp.Run();\n'})}),"\n",(0,i.jsx)(n.h2,{id:"6-inject-and-use-the-repository",children:"6. Inject and use the repository"}),"\n",(0,i.jsxs)(n.p,{children:["RCommon automatically registers ",(0,i.jsx)(n.code,{children:"ILinqRepository"})," for every entity once ",(0,i.jsx)(n.code,{children:"EFCorePerisistenceBuilder"})," is configured. Inject it directly into your controllers, application services, or handlers."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'using RCommon.Persistence.Crud;\nusing Microsoft.AspNetCore.Mvc;\n\n[ApiController]\n[Route("api/[controller]")]\npublic class ProductsController : ControllerBase\n{\n private readonly ILinqRepository _products;\n\n public ProductsController(ILinqRepository products)\n {\n _products = products;\n }\n\n [HttpGet]\n public async Task GetAll()\n {\n var products = await _products.FindAsync(p => p.Price > 0);\n return Ok(products);\n }\n\n [HttpGet("{id:guid}")]\n public async Task GetById(Guid id)\n {\n var product = await _products.FindAsync(id);\n if (product is null) return NotFound();\n return Ok(product);\n }\n\n [HttpPost]\n public async Task Create(Product product)\n {\n await _products.AddAsync(product);\n return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);\n }\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"what-happens-under-the-hood",children:"What happens under the hood"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"AddRCommon()"})," creates an ",(0,i.jsx)(n.code,{children:"RCommonBuilder"})," and registers the core services: the in-memory event bus, the ",(0,i.jsx)(n.code,{children:"EventSubscriptionManager"}),", and the ",(0,i.jsx)(n.code,{children:"InMemoryTransactionalEventRouter"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"WithSequentialGuidGenerator"})," registers ",(0,i.jsx)(n.code,{children:"IGuidGenerator"})," as a transient ",(0,i.jsx)(n.code,{children:"SequentialGuidGenerator"}),", useful for database-friendly ordered GUIDs."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"WithDateTimeSystem"})," registers ",(0,i.jsx)(n.code,{children:"ISystemTime"})," so that auditing and time-dependent logic is testable."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"WithPersistence"})," registers the open-generic repository types (",(0,i.jsx)(n.code,{children:"ILinqRepository<>"}),", ",(0,i.jsx)(n.code,{children:"IReadOnlyRepository<>"}),", ",(0,i.jsx)(n.code,{children:"IWriteOnlyRepository<>"}),") backed by EF Core, and maps the named DbContext to the data store factory."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"WithUnitOfWork"})," registers ",(0,i.jsx)(n.code,{children:"IUnitOfWork"})," and ",(0,i.jsx)(n.code,{children:"IUnitOfWorkFactory"})," for transactional coordination."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["From here you can add CQRS mediation, event handling, validation, or any other RCommon feature by chaining additional ",(0,i.jsx)(n.code,{children:"With*"})," calls on the builder."]}),"\n",(0,i.jsx)(n.admonition,{title:"Modular apps",type:"tip",children:(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"AddRCommon()"})," can be called from multiple modules safely \u2014 repeat calls return the same cached builder, sub-builder verbs (",(0,i.jsx)(n.code,{children:"WithPersistence"}),", ",(0,i.jsx)(n.code,{children:"WithMediator"}),", ",(0,i.jsx)(n.code,{children:"WithEventHandling"}),", etc.) reuse the same sub-builder, and singleton-style verbs are idempotent on identical impls. Registrations merge instead of throwing. See ",(0,i.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})})]})}function p(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(u,{...e})}):u(e)}},75783(e,n,t){t.d(n,{A:()=>l});var i=t(30758);const o="container_xjrG",r="label_Y4p8",s="commandRow_FY5I",a="command_m7Qs",c="copyButton_u1GK";var d=t(86070);function l({packageName:e,version:n}){const[t,l]=(0,i.useState)(!1),u=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,d.jsxs)("div",{className:o,children:[(0,d.jsx)("div",{className:r,children:"NuGet Package"}),(0,d.jsxs)("div",{className:s,children:[(0,d.jsx)("code",{className:a,children:u}),(0,d.jsx)("button",{className:c,onClick:()=>{navigator.clipboard.writeText(u),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:t?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,t){t.d(n,{R:()=>s,x:()=>a});var i=t(30758);const o={},r=i.createContext(o);function s(e){const n=i.useContext(r);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:s(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/053d6f47.c0f4d811.js b/website/build/assets/js/053d6f47.c0f4d811.js deleted file mode 100644 index b3d4119a..00000000 --- a/website/build/assets/js/053d6f47.c0f4d811.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[7768],{81993(e,n,t){t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>a,default:()=>p,frontMatter:()=>s,metadata:()=>c,toc:()=>l});var r=t(86070),i=t(81753),o=t(75783);const s={title:"Quick Start Guide",sidebar_position:3,description:"Build a .NET web API with RCommon in minutes \u2014 define entities, configure EF Core persistence, and query via ILinqRepository using a single fluent builder chain."},a="Quick Start Guide",c={id:"getting-started/quick-start",title:"Quick Start Guide",description:"Build a .NET web API with RCommon in minutes \u2014 define entities, configure EF Core persistence, and query via ILinqRepository using a single fluent builder chain.",source:"@site/docs/getting-started/quick-start.mdx",sourceDirName:"getting-started",slug:"/getting-started/quick-start",permalink:"/docs/next/getting-started/quick-start",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/getting-started/quick-start.mdx",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"Quick Start Guide",sidebar_position:3,description:"Build a .NET web API with RCommon in minutes \u2014 define entities, configure EF Core persistence, and query via ILinqRepository using a single fluent builder chain."},sidebar:"docsSidebar",previous:{title:"Installation",permalink:"/docs/next/getting-started/installation"},next:{title:"Configuration & Bootstrapping",permalink:"/docs/next/getting-started/configuration"}},d={},l=[{value:"1. Create a new project",id:"1-create-a-new-project",level:2},{value:"2. Install packages",id:"2-install-packages",level:2},{value:"3. Define your entity",id:"3-define-your-entity",level:2},{value:"4. Create a DbContext",id:"4-create-a-dbcontext",level:2},{value:"5. Configure RCommon in Program.cs",id:"5-configure-rcommon-in-programcs",level:2},{value:"6. Inject and use the repository",id:"6-inject-and-use-the-repository",level:2},{value:"What happens under the hood",id:"what-happens-under-the-hood",level:2}];function u(e){const n={code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",ul:"ul",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"quick-start-guide",children:"Quick Start Guide"}),"\n",(0,r.jsx)(n.p,{children:"This guide walks through the minimum steps to create a new .NET web API, add RCommon, and run a repository query against a SQL Server database using Entity Framework Core."}),"\n",(0,r.jsx)(n.h2,{id:"1-create-a-new-project",children:"1. Create a new project"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"dotnet new webapi -n MyApp\r\ncd MyApp\n"})}),"\n",(0,r.jsx)(n.h2,{id:"2-install-packages",children:"2. Install packages"}),"\n",(0,r.jsx)(o.A,{packageName:"RCommon.Core"}),"\n",(0,r.jsx)(o.A,{packageName:"RCommon.Entities"}),"\n",(0,r.jsx)(o.A,{packageName:"RCommon.Persistence"}),"\n",(0,r.jsx)(o.A,{packageName:"RCommon.EFCore"}),"\n",(0,r.jsx)(n.h2,{id:"3-define-your-entity",children:"3. Define your entity"}),"\n",(0,r.jsxs)(n.p,{children:["Entities in RCommon inherit from a base class provided by ",(0,r.jsx)(n.code,{children:"RCommon.Entities"}),". The base classes handle common concerns like auditing and domain events automatically."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Entities;\r\n\r\npublic class Product : BusinessEntity\r\n{\r\n public string Name { get; set; } = string.Empty;\r\n public decimal Price { get; set; }\r\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"4-create-a-dbcontext",children:"4. Create a DbContext"}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"RCommonDbContext"})," base class integrates with RCommon's change tracking and domain event dispatch."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"using Microsoft.EntityFrameworkCore;\r\nusing RCommon.Persistence.EFCore;\r\n\r\npublic class AppDbContext : RCommonDbContext\r\n{\r\n public AppDbContext(DbContextOptions options,\r\n IEntityEventTracker eventTracker)\r\n : base(options, eventTracker)\r\n {\r\n }\r\n\r\n public DbSet Products => Set();\r\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"5-configure-rcommon-in-programcs",children:"5. Configure RCommon in Program.cs"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon;\r\nusing RCommon.Persistence.EFCore;\r\nusing RCommon.Persistence.Transactions;\r\nusing Microsoft.EntityFrameworkCore;\r\nusing System.Transactions;\r\n\r\nvar builder = WebApplication.CreateBuilder(args);\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithSequentialGuidGenerator(guid =>\r\n guid.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString)\r\n .WithDateTimeSystem(dt => dt.Kind = DateTimeKind.Utc)\r\n .WithPersistence(ef =>\r\n {\r\n ef.AddDbContext("AppDb", options =>\r\n {\r\n options.UseSqlServer(\r\n builder.Configuration.GetConnectionString("DefaultConnection"));\r\n });\r\n ef.SetDefaultDataStore(ds =>\r\n {\r\n ds.DefaultDataStoreName = "AppDb";\r\n });\r\n })\r\n .WithUnitOfWork(uow =>\r\n {\r\n uow.SetOptions(options =>\r\n {\r\n options.AutoCompleteScope = true;\r\n options.DefaultIsolation = IsolationLevel.ReadCommitted;\r\n });\r\n });\r\n\r\nbuilder.Services.AddControllers();\r\n\r\nvar app = builder.Build();\r\napp.MapControllers();\r\napp.Run();\n'})}),"\n",(0,r.jsx)(n.h2,{id:"6-inject-and-use-the-repository",children:"6. Inject and use the repository"}),"\n",(0,r.jsxs)(n.p,{children:["RCommon automatically registers ",(0,r.jsx)(n.code,{children:"ILinqRepository"})," for every entity once ",(0,r.jsx)(n.code,{children:"EFCorePerisistenceBuilder"})," is configured. Inject it directly into your controllers, application services, or handlers."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon.Persistence.Crud;\r\nusing Microsoft.AspNetCore.Mvc;\r\n\r\n[ApiController]\r\n[Route("api/[controller]")]\r\npublic class ProductsController : ControllerBase\r\n{\r\n private readonly ILinqRepository _products;\r\n\r\n public ProductsController(ILinqRepository products)\r\n {\r\n _products = products;\r\n }\r\n\r\n [HttpGet]\r\n public async Task GetAll()\r\n {\r\n var products = await _products.FindAsync(p => p.Price > 0);\r\n return Ok(products);\r\n }\r\n\r\n [HttpGet("{id:guid}")]\r\n public async Task GetById(Guid id)\r\n {\r\n var product = await _products.FindAsync(id);\r\n if (product is null) return NotFound();\r\n return Ok(product);\r\n }\r\n\r\n [HttpPost]\r\n public async Task Create(Product product)\r\n {\r\n await _products.AddAsync(product);\r\n return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);\r\n }\r\n}\n'})}),"\n",(0,r.jsx)(n.h2,{id:"what-happens-under-the-hood",children:"What happens under the hood"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"AddRCommon()"})," creates an ",(0,r.jsx)(n.code,{children:"RCommonBuilder"})," and registers the core services: the in-memory event bus, the ",(0,r.jsx)(n.code,{children:"EventSubscriptionManager"}),", and the ",(0,r.jsx)(n.code,{children:"InMemoryTransactionalEventRouter"}),"."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"WithSequentialGuidGenerator"})," registers ",(0,r.jsx)(n.code,{children:"IGuidGenerator"})," as a transient ",(0,r.jsx)(n.code,{children:"SequentialGuidGenerator"}),", useful for database-friendly ordered GUIDs."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"WithDateTimeSystem"})," registers ",(0,r.jsx)(n.code,{children:"ISystemTime"})," so that auditing and time-dependent logic is testable."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"WithPersistence"})," registers the open-generic repository types (",(0,r.jsx)(n.code,{children:"ILinqRepository<>"}),", ",(0,r.jsx)(n.code,{children:"IReadOnlyRepository<>"}),", ",(0,r.jsx)(n.code,{children:"IWriteOnlyRepository<>"}),") backed by EF Core, and maps the named DbContext to the data store factory."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"WithUnitOfWork"})," registers ",(0,r.jsx)(n.code,{children:"IUnitOfWork"})," and ",(0,r.jsx)(n.code,{children:"IUnitOfWorkFactory"})," for transactional coordination."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["From here you can add CQRS mediation, event handling, validation, or any other RCommon feature by chaining additional ",(0,r.jsx)(n.code,{children:"With*"})," calls on the builder."]})]})}function p(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(u,{...e})}):u(e)}},75783(e,n,t){t.d(n,{A:()=>l});var r=t(30758);const i="container_xjrG",o="label_Y4p8",s="commandRow_FY5I",a="command_m7Qs",c="copyButton_u1GK";var d=t(86070);function l({packageName:e,version:n}){const[t,l]=(0,r.useState)(!1),u=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,d.jsxs)("div",{className:i,children:[(0,d.jsx)("div",{className:o,children:"NuGet Package"}),(0,d.jsxs)("div",{className:s,children:[(0,d.jsx)("code",{className:a,children:u}),(0,d.jsx)("button",{className:c,onClick:()=>{navigator.clipboard.writeText(u),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:t?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,t){t.d(n,{R:()=>s,x:()=>a});var r=t(30758);const i={},o=r.createContext(i);function s(e){const n=r.useContext(o);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),r.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/06c1478b.3598bb04.js b/website/build/assets/js/06c1478b.3598bb04.js deleted file mode 100644 index abea013d..00000000 --- a/website/build/assets/js/06c1478b.3598bb04.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[4096],{60089(e,n,i){i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>l,toc:()=>c});var s=i(86070),t=i(81753);const o={title:"Changelog",sidebar_position:2,description:"RCommon release history covering blob storage abstractions, multi-tenancy support, DDD soft delete, bulk operations, and semantic versioning via MinVer git tags."},r="Changelog",l={id:"api-reference/changelog",title:"Changelog",description:"RCommon release history covering blob storage abstractions, multi-tenancy support, DDD soft delete, bulk operations, and semantic versioning via MinVer git tags.",source:"@site/docs/api-reference/changelog.mdx",sourceDirName:"api-reference",slug:"/api-reference/changelog",permalink:"/docs/next/api-reference/changelog",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/api-reference/changelog.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Changelog",sidebar_position:2,description:"RCommon release history covering blob storage abstractions, multi-tenancy support, DDD soft delete, bulk operations, and semantic versioning via MinVer git tags."},sidebar:"docsSidebar",previous:{title:"NuGet Packages",permalink:"/docs/next/api-reference/nuget-packages"},next:{title:"Migration Guide",permalink:"/docs/next/api-reference/migration-guide"}},a={},c=[{value:"Recent Changes",id:"recent-changes",level:2},{value:"Blob Storage Abstractions",id:"blob-storage-abstractions",level:3},{value:"Multi-Tenancy Support",id:"multi-tenancy-support",level:3},{value:"Domain-Driven Design Support",id:"domain-driven-design-support",level:3},{value:"Repository Soft Delete Extras",id:"repository-soft-delete-extras",level:3},{value:"Editorconfig and Code Hygiene",id:"editorconfig-and-code-hygiene",level:3},{value:"Versioning",id:"versioning",level:2},{value:"Release Schedule",id:"release-schedule",level:2},{value:"Filing Issues",id:"filing-issues",level:2}];function d(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",strong:"strong",ul:"ul",...(0,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"changelog",children:"Changelog"}),"\n",(0,s.jsx)(n.p,{children:"The authoritative release history for all RCommon packages is maintained on GitHub Releases."}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Full release notes:"})," ",(0,s.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon/releases",children:"https://github.com/RCommon-Team/RCommon/releases"})]}),"\n",(0,s.jsx)(n.h2,{id:"recent-changes",children:"Recent Changes"}),"\n",(0,s.jsx)(n.h3,{id:"blob-storage-abstractions",children:"Blob Storage Abstractions"}),"\n",(0,s.jsxs)(n.p,{children:["Added ",(0,s.jsx)(n.code,{children:"RCommon.Blobs"})," as a provider-agnostic blob storage abstraction layer, with two concrete implementations:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"RCommon.Azure.Blobs"})," \u2014 Azure Blob Storage implementation"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"RCommon.Amazon.S3Objects"})," \u2014 Amazon S3 implementation"]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"multi-tenancy-support",children:"Multi-Tenancy Support"}),"\n",(0,s.jsx)(n.p,{children:"Added complete multi-tenancy infrastructure:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"RCommon.MultiTenancy"})," \u2014 builder abstraction for registering tenancy providers via ",(0,s.jsx)(n.code,{children:"WithMultiTenancy()"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"RCommon.Finbuckle"})," \u2014 Finbuckle.MultiTenant integration, providing ",(0,s.jsx)(n.code,{children:"FinbuckleTenantIdAccessor"})," that bridges Finbuckle's resolved tenant context to the ",(0,s.jsx)(n.code,{children:"ITenantIdAccessor"})," consumed by all repositories"]}),"\n",(0,s.jsxs)(n.li,{children:["All persistence providers (",(0,s.jsx)(n.code,{children:"EFCoreRepository"}),", ",(0,s.jsx)(n.code,{children:"DapperRepository"}),", ",(0,s.jsx)(n.code,{children:"Linq2DbRepository"}),") automatically filter reads by tenant and stamp writes with the current tenant ID when entities implement ",(0,s.jsx)(n.code,{children:"IMultiTenant"})]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"domain-driven-design-support",children:"Domain-Driven Design Support"}),"\n",(0,s.jsx)(n.p,{children:"Added soft delete and multitenancy to the entity layer:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"ISoftDelete"})," \u2014 opt-in interface for logical deletion; repositories set ",(0,s.jsx)(n.code,{children:"IsDeleted = true"})," and perform an UPDATE instead of a physical DELETE"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"IMultiTenant"})," \u2014 opt-in interface for tenant-scoped entities; repositories filter reads and stamp writes automatically"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"SoftDeleteHelper"})," and ",(0,s.jsx)(n.code,{children:"MultiTenantHelper"})," utility classes for filter expression building and entity stamping"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"EntityNotFoundException"}),' for consistent "entity not found" error handling with type and ID context']}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"repository-soft-delete-extras",children:"Repository Soft Delete Extras"}),"\n",(0,s.jsx)(n.p,{children:"Extended all persistence providers with explicit soft delete and bulk operation support:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"DeleteAsync(entity, isSoftDelete: bool)"})," overload on all write repositories"]}),"\n",(0,s.jsxs)(n.li,{children:["Automatic ",(0,s.jsx)(n.code,{children:"!IsDeleted"})," filtering on all read operations for ",(0,s.jsx)(n.code,{children:"ISoftDelete"})," entities"]}),"\n",(0,s.jsxs)(n.li,{children:["Bulk delete via ",(0,s.jsx)(n.code,{children:"ExecuteDeleteAsync"})," on ",(0,s.jsx)(n.code,{children:"EFCoreRepository"})]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"editorconfig-and-code-hygiene",children:"Editorconfig and Code Hygiene"}),"\n",(0,s.jsxs)(n.p,{children:["Added ",(0,s.jsx)(n.code,{children:".editorconfig"})," to enforce consistent code style across the solution. All public methods and complex code paths now carry XML documentation comments."]}),"\n",(0,s.jsx)(n.h2,{id:"versioning",children:"Versioning"}),"\n",(0,s.jsxs)(n.p,{children:["RCommon uses ",(0,s.jsx)(n.a,{href:"https://github.com/adamralph/minver",children:"MinVer"})," for automatic semantic versioning from git tags. All packages in the solution share the same version number."]}),"\n",(0,s.jsxs)(n.p,{children:["Version numbers follow ",(0,s.jsx)(n.a,{href:"https://semver.org/",children:"Semantic Versioning"}),":"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Major"})," (",(0,s.jsx)(n.code,{children:"x.0.0"}),") \u2014 breaking API changes"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Minor"})," (",(0,s.jsx)(n.code,{children:"0.x.0"}),") \u2014 new features, backward-compatible additions"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Patch"})," (",(0,s.jsx)(n.code,{children:"0.0.x"}),") \u2014 bug fixes and documentation updates"]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Pre-release versions (alpha, beta, rc) are tagged accordingly (e.g., ",(0,s.jsx)(n.code,{children:"v3.0.0-alpha.1"}),")."]}),"\n",(0,s.jsx)(n.h2,{id:"release-schedule",children:"Release Schedule"}),"\n",(0,s.jsxs)(n.p,{children:["RCommon does not follow a fixed release cadence. Releases are made when meaningful features, bug fixes, or breaking changes are ready. Subscribe to ",(0,s.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon/releases",children:"GitHub Releases"})," or watch the repository to be notified of new versions."]}),"\n",(0,s.jsx)(n.h2,{id:"filing-issues",children:"Filing Issues"}),"\n",(0,s.jsx)(n.p,{children:"Found a bug or missing a feature? Please open an issue on GitHub:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Bug reports:"})," ",(0,s.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon/issues/new",children:"https://github.com/RCommon-Team/RCommon/issues/new?template=bug_report.md"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Feature requests:"})," ",(0,s.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon/issues/new",children:"https://github.com/RCommon-Team/RCommon/issues/new?template=feature_request.md"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Discussions:"})," ",(0,s.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon/discussions",children:"https://github.com/RCommon-Team/RCommon/discussions"})]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},81753(e,n,i){i.d(n,{R:()=>r,x:()=>l});var s=i(30758);const t={},o=s.createContext(t);function r(e){const n=s.useContext(o);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:r(e.components),s.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/06c1478b.61376df1.js b/website/build/assets/js/06c1478b.61376df1.js new file mode 100644 index 00000000..3cc1d2b1 --- /dev/null +++ b/website/build/assets/js/06c1478b.61376df1.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[4096],{60089(e,n,i){i.r(n),i.d(n,{assets:()=>c,contentTitle:()=>r,default:()=>h,frontMatter:()=>t,metadata:()=>l,toc:()=>d});var s=i(86070),o=i(81753);const t={title:"Changelog",sidebar_position:2,description:"RCommon release history covering blob storage abstractions, multi-tenancy support, DDD soft delete, bulk operations, and semantic versioning via MinVer git tags."},r="Changelog",l={id:"api-reference/changelog",title:"Changelog",description:"RCommon release history covering blob storage abstractions, multi-tenancy support, DDD soft delete, bulk operations, and semantic versioning via MinVer git tags.",source:"@site/docs/api-reference/changelog.mdx",sourceDirName:"api-reference",slug:"/api-reference/changelog",permalink:"/docs/next/api-reference/changelog",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/api-reference/changelog.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Changelog",sidebar_position:2,description:"RCommon release history covering blob storage abstractions, multi-tenancy support, DDD soft delete, bulk operations, and semantic versioning via MinVer git tags."},sidebar:"docsSidebar",previous:{title:"NuGet Packages",permalink:"/docs/next/api-reference/nuget-packages"},next:{title:"Migration Guide",permalink:"/docs/next/api-reference/migration-guide"}},c={},d=[{value:"Recent Changes",id:"recent-changes",level:2},{value:"Modular Composition (Unreleased)",id:"modular-composition-unreleased",level:3},{value:"Blob Storage Abstractions",id:"blob-storage-abstractions",level:3},{value:"Multi-Tenancy Support",id:"multi-tenancy-support",level:3},{value:"Domain-Driven Design Support",id:"domain-driven-design-support",level:3},{value:"Repository Soft Delete Extras",id:"repository-soft-delete-extras",level:3},{value:"Editorconfig and Code Hygiene",id:"editorconfig-and-code-hygiene",level:3},{value:"Versioning",id:"versioning",level:2},{value:"Release Schedule",id:"release-schedule",level:2},{value:"Filing Issues",id:"filing-issues",level:2}];function a(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",strong:"strong",ul:"ul",...(0,o.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"changelog",children:"Changelog"}),"\n",(0,s.jsx)(n.p,{children:"The authoritative release history for all RCommon packages is maintained on GitHub Releases."}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.strong,{children:"Full release notes:"})," ",(0,s.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon/releases",children:"https://github.com/RCommon-Team/RCommon/releases"})]}),"\n",(0,s.jsx)(n.h2,{id:"recent-changes",children:"Recent Changes"}),"\n",(0,s.jsx)(n.h3,{id:"modular-composition-unreleased",children:"Modular Composition (Unreleased)"}),"\n",(0,s.jsxs)(n.p,{children:["Added support for calling ",(0,s.jsx)(n.code,{children:"services.AddRCommon()"})," from multiple modules in the same process. Registrations merge instead of duplicating or throwing on agreement."]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Added"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Modular composition support"})," \u2014 ",(0,s.jsx)(n.code,{children:"services.AddRCommon()"})," can now be called from multiple modules in the same process. Registrations merge instead of duplicating or throwing on agreement. See ",(0,s.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["New public API: ",(0,s.jsx)(n.code,{children:"IRCommonBuilder.GetOrAddBuilder(Func)"})," \u2014 third-party ",(0,s.jsx)(n.code,{children:"WithX"})," extensions opt into sub-builder caching."]}),"\n",(0,s.jsxs)(n.li,{children:["New public API: ",(0,s.jsx)(n.code,{children:"IRCommonBuilder.GetBootstrapDiagnostics()"})," \u2014 retrieve the soft-duplicate report stashed at host startup."]}),"\n",(0,s.jsxs)(n.li,{children:["New public API: ",(0,s.jsx)(n.code,{children:"IServiceCollection.IsRCommonInitialized()"})," \u2014 true iff ",(0,s.jsx)(n.code,{children:"AddRCommon()"})," has been called."]}),"\n",(0,s.jsxs)(n.li,{children:["New internal ",(0,s.jsx)(n.code,{children:"IHostedService"})," runs the duplicate-descriptor scanner at host startup and emits a single warning on soft duplicates."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Changed"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"AddRCommon()"})," is now idempotent: subsequent calls return the cached ",(0,s.jsx)(n.code,{children:"IRCommonBuilder"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["Singleton-style verbs (",(0,s.jsx)(n.code,{children:"WithSimpleGuidGenerator"}),", ",(0,s.jsx)(n.code,{children:"WithSequentialGuidGenerator"}),", ",(0,s.jsx)(n.code,{children:"WithDateTimeSystem"}),", ",(0,s.jsx)(n.code,{children:"WithJsonSerialization"}),", ",(0,s.jsx)(n.code,{children:"WithSmtpEmailServices"}),", ",(0,s.jsx)(n.code,{children:"WithSendGridEmailServices"}),") are now idempotent on same-type re-registration. Different-type conflicts throw ",(0,s.jsx)(n.code,{children:"RCommonBuilderException"})," with diagnostic messages."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"DataStoreFactoryOptions.Register<,>"})," is now idempotent for identical ",(0,s.jsx)(n.code,{children:"(name, base, concrete)"})," triples. Conflicts (same ",(0,s.jsx)(n.code,{children:"(name, base)"})," with different concrete) throw ",(0,s.jsx)(n.code,{children:"UnsupportedDataStoreException"})," (exception type preserved)."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"AddProducer"})," now deduplicates by concrete producer type via descriptor scan \u2014 distinct producer types still coexist."]}),"\n",(0,s.jsxs)(n.li,{children:["All sub-builder ",(0,s.jsx)(n.code,{children:"WithX"})," extensions route through ",(0,s.jsx)(n.code,{children:"GetOrAddBuilder"}),"; generic constraints tightened to ",(0,s.jsx)(n.code,{children:"where T : class, ..."}),"."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:(0,s.jsx)(n.strong,{children:"Notes"})}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Strictly additive public API surface. No method or interface signature removals."}),"\n",(0,s.jsx)(n.li,{children:"No exception types changed on existing APIs."}),"\n",(0,s.jsxs)(n.li,{children:["See ",(0,s.jsx)(n.code,{children:"Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/"})," for a runnable demonstration."]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"blob-storage-abstractions",children:"Blob Storage Abstractions"}),"\n",(0,s.jsxs)(n.p,{children:["Added ",(0,s.jsx)(n.code,{children:"RCommon.Blobs"})," as a provider-agnostic blob storage abstraction layer, with two concrete implementations:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"RCommon.Azure.Blobs"})," \u2014 Azure Blob Storage implementation"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"RCommon.Amazon.S3Objects"})," \u2014 Amazon S3 implementation"]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"multi-tenancy-support",children:"Multi-Tenancy Support"}),"\n",(0,s.jsx)(n.p,{children:"Added complete multi-tenancy infrastructure:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"RCommon.MultiTenancy"})," \u2014 builder abstraction for registering tenancy providers via ",(0,s.jsx)(n.code,{children:"WithMultiTenancy()"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"RCommon.Finbuckle"})," \u2014 Finbuckle.MultiTenant integration, providing ",(0,s.jsx)(n.code,{children:"FinbuckleTenantIdAccessor"})," that bridges Finbuckle's resolved tenant context to the ",(0,s.jsx)(n.code,{children:"ITenantIdAccessor"})," consumed by all repositories"]}),"\n",(0,s.jsxs)(n.li,{children:["All persistence providers (",(0,s.jsx)(n.code,{children:"EFCoreRepository"}),", ",(0,s.jsx)(n.code,{children:"DapperRepository"}),", ",(0,s.jsx)(n.code,{children:"Linq2DbRepository"}),") automatically filter reads by tenant and stamp writes with the current tenant ID when entities implement ",(0,s.jsx)(n.code,{children:"IMultiTenant"})]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"domain-driven-design-support",children:"Domain-Driven Design Support"}),"\n",(0,s.jsx)(n.p,{children:"Added soft delete and multitenancy to the entity layer:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"ISoftDelete"})," \u2014 opt-in interface for logical deletion; repositories set ",(0,s.jsx)(n.code,{children:"IsDeleted = true"})," and perform an UPDATE instead of a physical DELETE"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"IMultiTenant"})," \u2014 opt-in interface for tenant-scoped entities; repositories filter reads and stamp writes automatically"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"SoftDeleteHelper"})," and ",(0,s.jsx)(n.code,{children:"MultiTenantHelper"})," utility classes for filter expression building and entity stamping"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"EntityNotFoundException"}),' for consistent "entity not found" error handling with type and ID context']}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"repository-soft-delete-extras",children:"Repository Soft Delete Extras"}),"\n",(0,s.jsx)(n.p,{children:"Extended all persistence providers with explicit soft delete and bulk operation support:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"DeleteAsync(entity, isSoftDelete: bool)"})," overload on all write repositories"]}),"\n",(0,s.jsxs)(n.li,{children:["Automatic ",(0,s.jsx)(n.code,{children:"!IsDeleted"})," filtering on all read operations for ",(0,s.jsx)(n.code,{children:"ISoftDelete"})," entities"]}),"\n",(0,s.jsxs)(n.li,{children:["Bulk delete via ",(0,s.jsx)(n.code,{children:"ExecuteDeleteAsync"})," on ",(0,s.jsx)(n.code,{children:"EFCoreRepository"})]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"editorconfig-and-code-hygiene",children:"Editorconfig and Code Hygiene"}),"\n",(0,s.jsxs)(n.p,{children:["Added ",(0,s.jsx)(n.code,{children:".editorconfig"})," to enforce consistent code style across the solution. All public methods and complex code paths now carry XML documentation comments."]}),"\n",(0,s.jsx)(n.h2,{id:"versioning",children:"Versioning"}),"\n",(0,s.jsxs)(n.p,{children:["RCommon uses ",(0,s.jsx)(n.a,{href:"https://github.com/adamralph/minver",children:"MinVer"})," for automatic semantic versioning from git tags. All packages in the solution share the same version number."]}),"\n",(0,s.jsxs)(n.p,{children:["Version numbers follow ",(0,s.jsx)(n.a,{href:"https://semver.org/",children:"Semantic Versioning"}),":"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Major"})," (",(0,s.jsx)(n.code,{children:"x.0.0"}),") \u2014 breaking API changes"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Minor"})," (",(0,s.jsx)(n.code,{children:"0.x.0"}),") \u2014 new features, backward-compatible additions"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Patch"})," (",(0,s.jsx)(n.code,{children:"0.0.x"}),") \u2014 bug fixes and documentation updates"]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Pre-release versions (alpha, beta, rc) are tagged accordingly (e.g., ",(0,s.jsx)(n.code,{children:"v3.0.0-alpha.1"}),")."]}),"\n",(0,s.jsx)(n.h2,{id:"release-schedule",children:"Release Schedule"}),"\n",(0,s.jsxs)(n.p,{children:["RCommon does not follow a fixed release cadence. Releases are made when meaningful features, bug fixes, or breaking changes are ready. Subscribe to ",(0,s.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon/releases",children:"GitHub Releases"})," or watch the repository to be notified of new versions."]}),"\n",(0,s.jsx)(n.h2,{id:"filing-issues",children:"Filing Issues"}),"\n",(0,s.jsx)(n.p,{children:"Found a bug or missing a feature? Please open an issue on GitHub:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Bug reports:"})," ",(0,s.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon/issues/new",children:"https://github.com/RCommon-Team/RCommon/issues/new?template=bug_report.md"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Feature requests:"})," ",(0,s.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon/issues/new",children:"https://github.com/RCommon-Team/RCommon/issues/new?template=feature_request.md"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Discussions:"})," ",(0,s.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon/discussions",children:"https://github.com/RCommon-Team/RCommon/discussions"})]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(a,{...e})}):a(e)}},81753(e,n,i){i.d(n,{R:()=>r,x:()=>l});var s=i(30758);const o={},t=s.createContext(o);function r(e){const n=s.useContext(t);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),s.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/0e171350.a8de79e8.js b/website/build/assets/js/0e171350.a8de79e8.js deleted file mode 100644 index 64679d19..00000000 --- a/website/build/assets/js/0e171350.a8de79e8.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[2165],{1647(e,i,c){c.r(i),c.d(i,{assets:()=>a,contentTitle:()=>o,default:()=>m,frontMatter:()=>d,metadata:()=>t,toc:()=>h});var r=c(86070),n=c(81753),s=c(75783);const d={title:"Memory Cache",sidebar_position:2,description:"RCommon.MemoryCache provides in-process IMemoryCache and IDistributedCache-backed caching with get-or-create patterns and LINQ expression caching optimization."},o="Memory Cache",t={id:"caching/memory",title:"Memory Cache",description:"RCommon.MemoryCache provides in-process IMemoryCache and IDistributedCache-backed caching with get-or-create patterns and LINQ expression caching optimization.",source:"@site/docs/caching/memory.mdx",sourceDirName:"caching",slug:"/caching/memory",permalink:"/docs/next/caching/memory",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/caching/memory.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Memory Cache",sidebar_position:2,description:"RCommon.MemoryCache provides in-process IMemoryCache and IDistributedCache-backed caching with get-or-create patterns and LINQ expression caching optimization."},sidebar:"docsSidebar",previous:{title:"Overview",permalink:"/docs/next/caching/overview"},next:{title:"Redis Cache",permalink:"/docs/next/caching/redis"}},a={},h=[{value:"Installation",id:"installation",level:2},{value:"In-memory cache setup",id:"in-memory-cache-setup",level:2},{value:"Minimal setup",id:"minimal-setup",level:3},{value:"Distributed memory cache setup",id:"distributed-memory-cache-setup",level:2},{value:"Expression caching",id:"expression-caching",level:2},{value:"Using ICacheService directly",id:"using-icacheservice-directly",level:2},{value:"Eviction policies",id:"eviction-policies",level:2},{value:"API summary",id:"api-summary",level:2}];function l(e){const i={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,n.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(i.h1,{id:"memory-cache",children:"Memory Cache"}),"\n",(0,r.jsxs)(i.p,{children:[(0,r.jsx)(i.code,{children:"RCommon.MemoryCache"})," provides two in-process caching implementations behind the ",(0,r.jsx)(i.code,{children:"ICacheService"})," abstraction:"]}),"\n",(0,r.jsxs)(i.ul,{children:["\n",(0,r.jsxs)(i.li,{children:[(0,r.jsx)(i.strong,{children:(0,r.jsx)(i.code,{children:"InMemoryCacheService"})})," \u2014 backed by ",(0,r.jsx)(i.code,{children:"Microsoft.Extensions.Caching.Memory.IMemoryCache"})]}),"\n",(0,r.jsxs)(i.li,{children:[(0,r.jsx)(i.strong,{children:(0,r.jsx)(i.code,{children:"DistributedMemoryCacheService"})})," \u2014 backed by ",(0,r.jsx)(i.code,{children:"Microsoft.Extensions.Caching.Distributed.IDistributedCache"})," using an in-memory store"]}),"\n"]}),"\n",(0,r.jsxs)(i.p,{children:["Both serve single-process applications. Use ",(0,r.jsx)(i.code,{children:"DistributedMemoryCacheService"})," when your service registration already relies on ",(0,r.jsx)(i.code,{children:"IDistributedCache"})," and you do not yet need a real distributed store."]}),"\n",(0,r.jsx)(i.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(s.A,{packageName:"RCommon.MemoryCache"}),"\n",(0,r.jsx)(i.h2,{id:"in-memory-cache-setup",children:"In-memory cache setup"}),"\n",(0,r.jsxs)(i.p,{children:["Register the in-process memory cache with ",(0,r.jsx)(i.code,{children:"WithMemoryCaching"}),":"]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"using RCommon;\r\nusing RCommon.MemoryCache;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithMemoryCaching(cache =>\r\n {\r\n // Optional: configure MemoryCacheOptions\r\n cache.Configure(options =>\r\n {\r\n options.SizeLimit = 1024;\r\n options.CompactionPercentage = 0.25;\r\n });\r\n });\n"})}),"\n",(0,r.jsxs)(i.p,{children:["This registers ",(0,r.jsx)(i.code,{children:"IMemoryCache"})," (via ",(0,r.jsx)(i.code,{children:"AddMemoryCache"}),") and makes ",(0,r.jsx)(i.code,{children:"InMemoryCacheService"})," available as ",(0,r.jsx)(i.code,{children:"ICacheService"}),"."]}),"\n",(0,r.jsx)(i.h3,{id:"minimal-setup",children:"Minimal setup"}),"\n",(0,r.jsx)(i.p,{children:"If you only want the default options, omit the configuration delegate:"}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithMemoryCaching();\n"})}),"\n",(0,r.jsx)(i.h2,{id:"distributed-memory-cache-setup",children:"Distributed memory cache setup"}),"\n",(0,r.jsxs)(i.p,{children:["Use ",(0,r.jsx)(i.code,{children:"DistributedMemoryCacheBuilder"})," when you want the ",(0,r.jsx)(i.code,{children:"IDistributedCache"})," abstraction backed by an in-process store. This is useful during development or for services that share the ",(0,r.jsx)(i.code,{children:"IDistributedCache"})," interface with a Redis implementation in production:"]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"using RCommon;\r\nusing RCommon.MemoryCache;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithDistributedCaching(cache =>\r\n {\r\n cache.Configure(options =>\r\n {\r\n options.SizeLimit = 512 * 1024 * 1024; // 512 MB\r\n });\r\n });\n"})}),"\n",(0,r.jsxs)(i.p,{children:["This registers ",(0,r.jsx)(i.code,{children:"IDistributedCache"})," via ",(0,r.jsx)(i.code,{children:"AddDistributedMemoryCache"})," and makes ",(0,r.jsx)(i.code,{children:"DistributedMemoryCacheService"})," available as ",(0,r.jsx)(i.code,{children:"ICacheService"}),". Data is serialized to JSON before storage."]}),"\n",(0,r.jsx)(i.h2,{id:"expression-caching",children:"Expression caching"}),"\n",(0,r.jsxs)(i.p,{children:["RCommon uses ",(0,r.jsx)(i.code,{children:"ICacheService"})," internally to cache compiled LINQ expression trees and reflection results. Call ",(0,r.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions"})," to activate this optimization:"]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithMemoryCaching(cache =>\r\n {\r\n cache.CacheDynamicallyCompiledExpressions();\r\n });\n"})}),"\n",(0,r.jsx)(i.p,{children:"This is the recommended minimum caching configuration for applications that use the persistence layer. It enables:"}),"\n",(0,r.jsxs)(i.ul,{children:["\n",(0,r.jsxs)(i.li,{children:[(0,r.jsx)(i.code,{children:"ICacheService"})," registered as ",(0,r.jsx)(i.code,{children:"InMemoryCacheService"})]}),"\n",(0,r.jsxs)(i.li,{children:[(0,r.jsx)(i.code,{children:"CachingOptions"})," with ",(0,r.jsx)(i.code,{children:"CachingEnabled = true"})," and ",(0,r.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions = true"})]}),"\n",(0,r.jsxs)(i.li,{children:["A ",(0,r.jsx)(i.code,{children:"Func"})," factory for strategy-based cache resolution"]}),"\n"]}),"\n",(0,r.jsxs)(i.p,{children:["The same method is available on ",(0,r.jsx)(i.code,{children:"DistributedMemoryCacheBuilder"}),":"]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithDistributedCaching(cache =>\r\n {\r\n cache.CacheDynamicallyCompiledExpressions();\r\n });\n"})}),"\n",(0,r.jsx)(i.h2,{id:"using-icacheservice-directly",children:"Using ICacheService directly"}),"\n",(0,r.jsxs)(i.p,{children:["Inject ",(0,r.jsx)(i.code,{children:"ICacheService"})," wherever you need caching. The get-or-create pattern eliminates the need for manual null checks:"]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:'public class ProductCatalogService\r\n{\r\n private readonly ICacheService _cache;\r\n private readonly IProductRepository _repository;\r\n\r\n public ProductCatalogService(ICacheService cache, IProductRepository repository)\r\n {\r\n _cache = cache;\r\n _repository = repository;\r\n }\r\n\r\n public async Task> GetActiveProductsAsync(CancellationToken ct)\r\n {\r\n var key = CacheKey.With(typeof(ProductCatalogService), "active-products");\r\n\r\n return await _cache.GetOrCreateAsync(key, () =>\r\n {\r\n return _repository.FindAsync(p => p.IsActive).Result;\r\n });\r\n }\r\n}\n'})}),"\n",(0,r.jsx)(i.h2,{id:"eviction-policies",children:"Eviction policies"}),"\n",(0,r.jsxs)(i.p,{children:["Eviction is controlled through ",(0,r.jsx)(i.code,{children:"MemoryCacheOptions"})," when calling ",(0,r.jsx)(i.code,{children:".Configure(options => ...)"}),":"]}),"\n",(0,r.jsxs)(i.table,{children:[(0,r.jsx)(i.thead,{children:(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.th,{children:"Option"}),(0,r.jsx)(i.th,{children:"Description"})]})}),(0,r.jsxs)(i.tbody,{children:[(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"SizeLimit"})}),(0,r.jsx)(i.td,{children:"Maximum number of cache entries (requires each entry to set a size)"})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"CompactionPercentage"})}),(0,r.jsxs)(i.td,{children:["Fraction of entries removed when the cache exceeds ",(0,r.jsx)(i.code,{children:"SizeLimit"})," (default 0.05)"]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"ExpirationScanFrequency"})}),(0,r.jsx)(i.td,{children:"How often the cache scans for expired entries (default 1 minute)"})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"TrackStatistics"})}),(0,r.jsxs)(i.td,{children:["Enables hit/miss statistics via ",(0,r.jsx)(i.code,{children:"IMemoryCache.GetCurrentStatistics()"})]})]})]})]}),"\n",(0,r.jsxs)(i.p,{children:["Individual entry expiration is configured through the ",(0,r.jsx)(i.code,{children:"IMemoryCache.GetOrCreate"})," callback (the underlying ",(0,r.jsx)(i.code,{children:"ICacheEntry"})," object). ",(0,r.jsx)(i.code,{children:"InMemoryCacheService"})," delegates directly to ",(0,r.jsx)(i.code,{children:"IMemoryCache.GetOrCreate"}),", so you can wrap it to set per-entry options if required."]}),"\n",(0,r.jsx)(i.h2,{id:"api-summary",children:"API summary"}),"\n",(0,r.jsxs)(i.table,{children:[(0,r.jsx)(i.thead,{children:(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.th,{children:"Type"}),(0,r.jsx)(i.th,{children:"Package"}),(0,r.jsx)(i.th,{children:"Description"})]})}),(0,r.jsxs)(i.tbody,{children:[(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"InMemoryCachingBuilder"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,r.jsxs)(i.td,{children:["Concrete builder for ",(0,r.jsx)(i.code,{children:"IMemoryCache"}),"-backed caching"]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"IInMemoryCachingBuilder"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,r.jsxs)(i.td,{children:["Marker interface extending ",(0,r.jsx)(i.code,{children:"IMemoryCachingBuilder"})]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"InMemoryCacheService"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,r.jsxs)(i.td,{children:[(0,r.jsx)(i.code,{children:"ICacheService"})," implementation backed by ",(0,r.jsx)(i.code,{children:"IMemoryCache"})]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"DistributedMemoryCacheBuilder"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,r.jsxs)(i.td,{children:["Concrete builder for in-process ",(0,r.jsx)(i.code,{children:"IDistributedCache"}),"-backed caching"]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"IDistributedMemoryCachingBuilder"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,r.jsxs)(i.td,{children:["Marker interface extending ",(0,r.jsx)(i.code,{children:"IDistributedCachingBuilder"})]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"DistributedMemoryCacheService"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,r.jsxs)(i.td,{children:[(0,r.jsx)(i.code,{children:"ICacheService"})," implementation backed by ",(0,r.jsx)(i.code,{children:"IDistributedCache"})," with JSON serialization"]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"Configure(options)"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,r.jsxs)(i.td,{children:["Extension on ",(0,r.jsx)(i.code,{children:"IInMemoryCachingBuilder"}),"; configures ",(0,r.jsx)(i.code,{children:"MemoryCacheOptions"})]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions()"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,r.jsx)(i.td,{children:"Extension enabling expression caching optimization"})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"ICacheService"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.Caching"})}),(0,r.jsx)(i.td,{children:"Core abstraction for get-or-create caching"})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"CacheKey"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.Caching"})}),(0,r.jsx)(i.td,{children:"Strongly-typed cache key with factory methods"})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"WithMemoryCaching()"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.Caching"})}),(0,r.jsxs)(i.td,{children:["Extension method on ",(0,r.jsx)(i.code,{children:"IRCommonBuilder"})]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"WithDistributedCaching()"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.Caching"})}),(0,r.jsxs)(i.td,{children:["Extension method on ",(0,r.jsx)(i.code,{children:"IRCommonBuilder"})]})]})]})]})]})}function m(e={}){const{wrapper:i}={...(0,n.R)(),...e.components};return i?(0,r.jsx)(i,{...e,children:(0,r.jsx)(l,{...e})}):l(e)}},75783(e,i,c){c.d(i,{A:()=>h});var r=c(30758);const n="container_xjrG",s="label_Y4p8",d="commandRow_FY5I",o="command_m7Qs",t="copyButton_u1GK";var a=c(86070);function h({packageName:e,version:i}){const[c,h]=(0,r.useState)(!1),l=i?`dotnet add package ${e} --version ${i}`:`dotnet add package ${e}`;return(0,a.jsxs)("div",{className:n,children:[(0,a.jsx)("div",{className:s,children:"NuGet Package"}),(0,a.jsxs)("div",{className:d,children:[(0,a.jsx)("code",{className:o,children:l}),(0,a.jsx)("button",{className:t,onClick:()=>{navigator.clipboard.writeText(l),h(!0),setTimeout(()=>h(!1),2e3)},title:"Copy to clipboard",children:c?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,i,c){c.d(i,{R:()=>d,x:()=>o});var r=c(30758);const n={},s=r.createContext(n);function d(e){const i=r.useContext(s);return r.useMemo(function(){return"function"==typeof e?e(i):{...i,...e}},[i,e])}function o(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:d(e.components),r.createElement(s.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/0e171350.c5b2e15a.js b/website/build/assets/js/0e171350.c5b2e15a.js new file mode 100644 index 00000000..81bc2552 --- /dev/null +++ b/website/build/assets/js/0e171350.c5b2e15a.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[2165],{1647(e,i,c){c.r(i),c.d(i,{assets:()=>a,contentTitle:()=>o,default:()=>m,frontMatter:()=>d,metadata:()=>t,toc:()=>h});var n=c(86070),r=c(81753),s=c(75783);const d={title:"Memory Cache",sidebar_position:2,description:"RCommon.MemoryCache provides in-process IMemoryCache and IDistributedCache-backed caching with get-or-create patterns and LINQ expression caching optimization."},o="Memory Cache",t={id:"caching/memory",title:"Memory Cache",description:"RCommon.MemoryCache provides in-process IMemoryCache and IDistributedCache-backed caching with get-or-create patterns and LINQ expression caching optimization.",source:"@site/docs/caching/memory.mdx",sourceDirName:"caching",slug:"/caching/memory",permalink:"/docs/next/caching/memory",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/caching/memory.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Memory Cache",sidebar_position:2,description:"RCommon.MemoryCache provides in-process IMemoryCache and IDistributedCache-backed caching with get-or-create patterns and LINQ expression caching optimization."},sidebar:"docsSidebar",previous:{title:"Overview",permalink:"/docs/next/caching/overview"},next:{title:"Redis Cache",permalink:"/docs/next/caching/redis"}},a={},h=[{value:"Installation",id:"installation",level:2},{value:"In-memory cache setup",id:"in-memory-cache-setup",level:2},{value:"Minimal setup",id:"minimal-setup",level:3},{value:"Distributed memory cache setup",id:"distributed-memory-cache-setup",level:2},{value:"Expression caching",id:"expression-caching",level:2},{value:"Using ICacheService directly",id:"using-icacheservice-directly",level:2},{value:"Eviction policies",id:"eviction-policies",level:2},{value:"API summary",id:"api-summary",level:2}];function l(e){const i={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(i.h1,{id:"memory-cache",children:"Memory Cache"}),"\n",(0,n.jsxs)(i.p,{children:[(0,n.jsx)(i.code,{children:"RCommon.MemoryCache"})," provides two in-process caching implementations behind the ",(0,n.jsx)(i.code,{children:"ICacheService"})," abstraction:"]}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:(0,n.jsx)(i.code,{children:"InMemoryCacheService"})})," \u2014 backed by ",(0,n.jsx)(i.code,{children:"Microsoft.Extensions.Caching.Memory.IMemoryCache"})]}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.strong,{children:(0,n.jsx)(i.code,{children:"DistributedMemoryCacheService"})})," \u2014 backed by ",(0,n.jsx)(i.code,{children:"Microsoft.Extensions.Caching.Distributed.IDistributedCache"})," using an in-memory store"]}),"\n"]}),"\n",(0,n.jsxs)(i.p,{children:["Both serve single-process applications. Use ",(0,n.jsx)(i.code,{children:"DistributedMemoryCacheService"})," when your service registration already relies on ",(0,n.jsx)(i.code,{children:"IDistributedCache"})," and you do not yet need a real distributed store."]}),"\n",(0,n.jsx)(i.h2,{id:"installation",children:"Installation"}),"\n",(0,n.jsx)(s.A,{packageName:"RCommon.MemoryCache"}),"\n",(0,n.jsx)(i.h2,{id:"in-memory-cache-setup",children:"In-memory cache setup"}),"\n",(0,n.jsxs)(i.p,{children:["Register the in-process memory cache with ",(0,n.jsx)(i.code,{children:"WithMemoryCaching"}),":"]}),"\n",(0,n.jsx)(i.pre,{children:(0,n.jsx)(i.code,{className:"language-csharp",children:"using RCommon;\nusing RCommon.MemoryCache;\n\nbuilder.Services.AddRCommon()\n .WithMemoryCaching(cache =>\n {\n // Optional: configure MemoryCacheOptions\n cache.Configure(options =>\n {\n options.SizeLimit = 1024;\n options.CompactionPercentage = 0.25;\n });\n });\n"})}),"\n",(0,n.jsxs)(i.p,{children:["This registers ",(0,n.jsx)(i.code,{children:"IMemoryCache"})," (via ",(0,n.jsx)(i.code,{children:"AddMemoryCache"}),") and makes ",(0,n.jsx)(i.code,{children:"InMemoryCacheService"})," available as ",(0,n.jsx)(i.code,{children:"ICacheService"}),"."]}),"\n",(0,n.jsx)(i.h3,{id:"minimal-setup",children:"Minimal setup"}),"\n",(0,n.jsx)(i.p,{children:"If you only want the default options, omit the configuration delegate:"}),"\n",(0,n.jsx)(i.pre,{children:(0,n.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithMemoryCaching();\n"})}),"\n",(0,n.jsx)(i.h2,{id:"distributed-memory-cache-setup",children:"Distributed memory cache setup"}),"\n",(0,n.jsxs)(i.p,{children:["Use ",(0,n.jsx)(i.code,{children:"DistributedMemoryCacheBuilder"})," when you want the ",(0,n.jsx)(i.code,{children:"IDistributedCache"})," abstraction backed by an in-process store. This is useful during development or for services that share the ",(0,n.jsx)(i.code,{children:"IDistributedCache"})," interface with a Redis implementation in production:"]}),"\n",(0,n.jsx)(i.pre,{children:(0,n.jsx)(i.code,{className:"language-csharp",children:"using RCommon;\nusing RCommon.MemoryCache;\n\nbuilder.Services.AddRCommon()\n .WithDistributedCaching(cache =>\n {\n cache.Configure(options =>\n {\n options.SizeLimit = 512 * 1024 * 1024; // 512 MB\n });\n });\n"})}),"\n",(0,n.jsxs)(i.p,{children:["This registers ",(0,n.jsx)(i.code,{children:"IDistributedCache"})," via ",(0,n.jsx)(i.code,{children:"AddDistributedMemoryCache"})," and makes ",(0,n.jsx)(i.code,{children:"DistributedMemoryCacheService"})," available as ",(0,n.jsx)(i.code,{children:"ICacheService"}),". Data is serialized to JSON before storage."]}),"\n",(0,n.jsx)(i.admonition,{title:"Modular composition",type:"tip",children:(0,n.jsxs)(i.p,{children:[(0,n.jsx)(i.code,{children:"WithMemoryCaching"})," and ",(0,n.jsx)(i.code,{children:"WithDistributedCaching"})," are cache-aware sub-builder verbs. When multiple modules call the same verb with the same builder type, the cached sub-builder is reused and each module's ",(0,n.jsx)(i.code,{children:"Configure(...)"})," and ",(0,n.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions()"}),' calls accumulate against one instance. This lets an "infrastructure" module enable expression caching while a "feature" module separately tunes ',(0,n.jsx)(i.code,{children:"MemoryCacheOptions"}),". See ",(0,n.jsx)(i.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,n.jsx)(i.h2,{id:"expression-caching",children:"Expression caching"}),"\n",(0,n.jsxs)(i.p,{children:["RCommon uses ",(0,n.jsx)(i.code,{children:"ICacheService"})," internally to cache compiled LINQ expression trees and reflection results. Call ",(0,n.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions"})," to activate this optimization:"]}),"\n",(0,n.jsx)(i.pre,{children:(0,n.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithMemoryCaching(cache =>\n {\n cache.CacheDynamicallyCompiledExpressions();\n });\n"})}),"\n",(0,n.jsx)(i.p,{children:"This is the recommended minimum caching configuration for applications that use the persistence layer. It enables:"}),"\n",(0,n.jsxs)(i.ul,{children:["\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.code,{children:"ICacheService"})," registered as ",(0,n.jsx)(i.code,{children:"InMemoryCacheService"})]}),"\n",(0,n.jsxs)(i.li,{children:[(0,n.jsx)(i.code,{children:"CachingOptions"})," with ",(0,n.jsx)(i.code,{children:"CachingEnabled = true"})," and ",(0,n.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions = true"})]}),"\n",(0,n.jsxs)(i.li,{children:["A ",(0,n.jsx)(i.code,{children:"Func"})," factory for strategy-based cache resolution"]}),"\n"]}),"\n",(0,n.jsxs)(i.p,{children:["The same method is available on ",(0,n.jsx)(i.code,{children:"DistributedMemoryCacheBuilder"}),":"]}),"\n",(0,n.jsx)(i.pre,{children:(0,n.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithDistributedCaching(cache =>\n {\n cache.CacheDynamicallyCompiledExpressions();\n });\n"})}),"\n",(0,n.jsx)(i.h2,{id:"using-icacheservice-directly",children:"Using ICacheService directly"}),"\n",(0,n.jsxs)(i.p,{children:["Inject ",(0,n.jsx)(i.code,{children:"ICacheService"})," wherever you need caching. The get-or-create pattern eliminates the need for manual null checks:"]}),"\n",(0,n.jsx)(i.pre,{children:(0,n.jsx)(i.code,{className:"language-csharp",children:'public class ProductCatalogService\n{\n private readonly ICacheService _cache;\n private readonly IProductRepository _repository;\n\n public ProductCatalogService(ICacheService cache, IProductRepository repository)\n {\n _cache = cache;\n _repository = repository;\n }\n\n public async Task> GetActiveProductsAsync(CancellationToken ct)\n {\n var key = CacheKey.With(typeof(ProductCatalogService), "active-products");\n\n return await _cache.GetOrCreateAsync(key, () =>\n {\n return _repository.FindAsync(p => p.IsActive).Result;\n });\n }\n}\n'})}),"\n",(0,n.jsx)(i.h2,{id:"eviction-policies",children:"Eviction policies"}),"\n",(0,n.jsxs)(i.p,{children:["Eviction is controlled through ",(0,n.jsx)(i.code,{children:"MemoryCacheOptions"})," when calling ",(0,n.jsx)(i.code,{children:".Configure(options => ...)"}),":"]}),"\n",(0,n.jsxs)(i.table,{children:[(0,n.jsx)(i.thead,{children:(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.th,{children:"Option"}),(0,n.jsx)(i.th,{children:"Description"})]})}),(0,n.jsxs)(i.tbody,{children:[(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"SizeLimit"})}),(0,n.jsx)(i.td,{children:"Maximum number of cache entries (requires each entry to set a size)"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"CompactionPercentage"})}),(0,n.jsxs)(i.td,{children:["Fraction of entries removed when the cache exceeds ",(0,n.jsx)(i.code,{children:"SizeLimit"})," (default 0.05)"]})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"ExpirationScanFrequency"})}),(0,n.jsx)(i.td,{children:"How often the cache scans for expired entries (default 1 minute)"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"TrackStatistics"})}),(0,n.jsxs)(i.td,{children:["Enables hit/miss statistics via ",(0,n.jsx)(i.code,{children:"IMemoryCache.GetCurrentStatistics()"})]})]})]})]}),"\n",(0,n.jsxs)(i.p,{children:["Individual entry expiration is configured through the ",(0,n.jsx)(i.code,{children:"IMemoryCache.GetOrCreate"})," callback (the underlying ",(0,n.jsx)(i.code,{children:"ICacheEntry"})," object). ",(0,n.jsx)(i.code,{children:"InMemoryCacheService"})," delegates directly to ",(0,n.jsx)(i.code,{children:"IMemoryCache.GetOrCreate"}),", so you can wrap it to set per-entry options if required."]}),"\n",(0,n.jsx)(i.h2,{id:"api-summary",children:"API summary"}),"\n",(0,n.jsxs)(i.table,{children:[(0,n.jsx)(i.thead,{children:(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.th,{children:"Type"}),(0,n.jsx)(i.th,{children:"Package"}),(0,n.jsx)(i.th,{children:"Description"})]})}),(0,n.jsxs)(i.tbody,{children:[(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"InMemoryCachingBuilder"})}),(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,n.jsxs)(i.td,{children:["Concrete builder for ",(0,n.jsx)(i.code,{children:"IMemoryCache"}),"-backed caching"]})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"IInMemoryCachingBuilder"})}),(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,n.jsxs)(i.td,{children:["Marker interface extending ",(0,n.jsx)(i.code,{children:"IMemoryCachingBuilder"})]})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"InMemoryCacheService"})}),(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,n.jsxs)(i.td,{children:[(0,n.jsx)(i.code,{children:"ICacheService"})," implementation backed by ",(0,n.jsx)(i.code,{children:"IMemoryCache"})]})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"DistributedMemoryCacheBuilder"})}),(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,n.jsxs)(i.td,{children:["Concrete builder for in-process ",(0,n.jsx)(i.code,{children:"IDistributedCache"}),"-backed caching"]})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"IDistributedMemoryCachingBuilder"})}),(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,n.jsxs)(i.td,{children:["Marker interface extending ",(0,n.jsx)(i.code,{children:"IDistributedCachingBuilder"})]})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"DistributedMemoryCacheService"})}),(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,n.jsxs)(i.td,{children:[(0,n.jsx)(i.code,{children:"ICacheService"})," implementation backed by ",(0,n.jsx)(i.code,{children:"IDistributedCache"})," with JSON serialization"]})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"Configure(options)"})}),(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,n.jsxs)(i.td,{children:["Extension on ",(0,n.jsx)(i.code,{children:"IInMemoryCachingBuilder"}),"; configures ",(0,n.jsx)(i.code,{children:"MemoryCacheOptions"})]})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions()"})}),(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,n.jsx)(i.td,{children:"Extension enabling expression caching optimization"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"ICacheService"})}),(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"RCommon.Caching"})}),(0,n.jsx)(i.td,{children:"Core abstraction for get-or-create caching"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"CacheKey"})}),(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"RCommon.Caching"})}),(0,n.jsx)(i.td,{children:"Strongly-typed cache key with factory methods"})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"WithMemoryCaching()"})}),(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"RCommon.Caching"})}),(0,n.jsxs)(i.td,{children:["Extension method on ",(0,n.jsx)(i.code,{children:"IRCommonBuilder"})]})]}),(0,n.jsxs)(i.tr,{children:[(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"WithDistributedCaching()"})}),(0,n.jsx)(i.td,{children:(0,n.jsx)(i.code,{children:"RCommon.Caching"})}),(0,n.jsxs)(i.td,{children:["Extension method on ",(0,n.jsx)(i.code,{children:"IRCommonBuilder"})]})]})]})]})]})}function m(e={}){const{wrapper:i}={...(0,r.R)(),...e.components};return i?(0,n.jsx)(i,{...e,children:(0,n.jsx)(l,{...e})}):l(e)}},75783(e,i,c){c.d(i,{A:()=>h});var n=c(30758);const r="container_xjrG",s="label_Y4p8",d="commandRow_FY5I",o="command_m7Qs",t="copyButton_u1GK";var a=c(86070);function h({packageName:e,version:i}){const[c,h]=(0,n.useState)(!1),l=i?`dotnet add package ${e} --version ${i}`:`dotnet add package ${e}`;return(0,a.jsxs)("div",{className:r,children:[(0,a.jsx)("div",{className:s,children:"NuGet Package"}),(0,a.jsxs)("div",{className:d,children:[(0,a.jsx)("code",{className:o,children:l}),(0,a.jsx)("button",{className:t,onClick:()=>{navigator.clipboard.writeText(l),h(!0),setTimeout(()=>h(!1),2e3)},title:"Copy to clipboard",children:c?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,i,c){c.d(i,{R:()=>d,x:()=>o});var n=c(30758);const r={},s=n.createContext(r);function d(e){const i=n.useContext(s);return n.useMemo(function(){return"function"==typeof e?e(i):{...i,...e}},[i,e])}function o(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:d(e.components),n.createElement(s.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/10134e3e.90eedb96.js b/website/build/assets/js/10134e3e.90eedb96.js deleted file mode 100644 index a9fe32de..00000000 --- a/website/build/assets/js/10134e3e.90eedb96.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[9865],{46984(e,r,i){i.r(r),i.d(r,{assets:()=>c,contentTitle:()=>o,default:()=>u,frontMatter:()=>t,metadata:()=>a,toc:()=>l});var n=i(86070),d=i(81753),s=i(75783);const t={title:"MediatR",sidebar_position:4,description:"Wire MediatR into RCommon's mediator abstraction for request-response and fan-out notifications, with built-in logging, validation, and unit-of-work pipeline behaviors."},o="MediatR",a={id:"cqrs-mediator/mediatr",title:"MediatR",description:"Wire MediatR into RCommon's mediator abstraction for request-response and fan-out notifications, with built-in logging, validation, and unit-of-work pipeline behaviors.",source:"@site/docs/cqrs-mediator/mediatr.mdx",sourceDirName:"cqrs-mediator",slug:"/cqrs-mediator/mediatr",permalink:"/docs/next/cqrs-mediator/mediatr",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/cqrs-mediator/mediatr.mdx",tags:[],version:"current",sidebarPosition:4,frontMatter:{title:"MediatR",sidebar_position:4,description:"Wire MediatR into RCommon's mediator abstraction for request-response and fan-out notifications, with built-in logging, validation, and unit-of-work pipeline behaviors."},sidebar:"docsSidebar",previous:{title:"Queries & Handlers",permalink:"/docs/next/cqrs-mediator/queries-handlers"},next:{title:"Wolverine",permalink:"/docs/next/cqrs-mediator/wolverine"}},c={},l=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Advanced MediatR configuration",id:"advanced-mediatr-configuration",level:3},{value:"Adding pipeline behaviors",id:"adding-pipeline-behaviors",level:3},{value:"Usage",id:"usage",level:2},{value:"Defining requests",id:"defining-requests",level:3},{value:"Defining request handlers",id:"defining-request-handlers",level:3},{value:"Defining notifications",id:"defining-notifications",level:3},{value:"Sending and publishing",id:"sending-and-publishing",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"IMediatorService",id:"imediatorservice",level:3},{value:"IMediatRBuilder registration methods",id:"imediatrbuilder-registration-methods",level:3},{value:"Built-in pipeline behaviors",id:"built-in-pipeline-behaviors",level:3},{value:"Marker interfaces",id:"marker-interfaces",level:3}];function h(e){const r={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.R)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(r.h1,{id:"mediatr",children:"MediatR"}),"\n",(0,n.jsx)(r.h2,{id:"overview",children:"Overview"}),"\n",(0,n.jsxs)(r.p,{children:[(0,n.jsx)(r.code,{children:"RCommon.MediatR"})," adapts the ",(0,n.jsx)(r.a,{href:"https://github.com/jbogard/MediatR",children:"MediatR"})," library to RCommon's mediator abstraction. It provides:"]}),"\n",(0,n.jsxs)(r.ul,{children:["\n",(0,n.jsxs)(r.li,{children:[(0,n.jsx)(r.code,{children:"MediatRAdapter"})," \u2014 an ",(0,n.jsx)(r.code,{children:"IMediatorAdapter"})," implementation that delegates ",(0,n.jsx)(r.code,{children:"Send"})," and ",(0,n.jsx)(r.code,{children:"Publish"})," calls to MediatR's ",(0,n.jsx)(r.code,{children:"IMediator"}),"."]}),"\n",(0,n.jsxs)(r.li,{children:[(0,n.jsx)(r.code,{children:"MediatRBuilder"})," \u2014 an ",(0,n.jsx)(r.code,{children:"IMediatRBuilder"})," that wires MediatR into the RCommon service container."]}),"\n",(0,n.jsx)(r.li,{children:"Pre-built pipeline behaviors for logging, validation, and unit-of-work."}),"\n",(0,n.jsxs)(r.li,{children:["Wrapper types (",(0,n.jsx)(r.code,{children:"MediatRRequest"}),", ",(0,n.jsx)(r.code,{children:"MediatRNotification"}),") that bridge RCommon message contracts to MediatR's internal types."]}),"\n"]}),"\n",(0,n.jsxs)(r.p,{children:["Application code depends only on ",(0,n.jsx)(r.code,{children:"IMediatorService"})," from ",(0,n.jsx)(r.code,{children:"RCommon.Mediator"}),". The MediatR implementation is an infrastructure detail registered at startup."]}),"\n",(0,n.jsx)(r.h2,{id:"installation",children:"Installation"}),"\n",(0,n.jsx)(s.A,{packageName:"RCommon.MediatR"}),"\n",(0,n.jsxs)(r.p,{children:["This package depends on ",(0,n.jsx)(r.code,{children:"RCommon.Mediator"}),", ",(0,n.jsx)(r.code,{children:"RCommon.ApplicationServices"}),", ",(0,n.jsx)(r.code,{children:"RCommon.Core"}),", ",(0,n.jsx)(r.code,{children:"RCommon.Persistence"}),", and the ",(0,n.jsx)(r.code,{children:"MediatR"})," NuGet package. All are pulled in transitively."]}),"\n",(0,n.jsx)(r.h2,{id:"configuration",children:"Configuration"}),"\n",(0,n.jsxs)(r.p,{children:["Register the MediatR provider inside ",(0,n.jsx)(r.code,{children:"AddRCommon()"})," using ",(0,n.jsx)(r.code,{children:"WithMediator"}),". The configuration delegate is where you register your requests, notifications, and pipeline behaviors."]}),"\n",(0,n.jsx)(r.pre,{children:(0,n.jsx)(r.code,{className:"language-csharp",children:"using RCommon;\r\nusing RCommon.Mediator.MediatR;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithMediator(mediator =>\r\n {\r\n // Register a fire-and-forget request (no response)\r\n mediator.AddRequest();\r\n\r\n // Register a request that returns a response\r\n mediator.AddRequest();\r\n\r\n // Register a notification (fan-out to all subscribers)\r\n mediator.AddNotification();\r\n });\n"})}),"\n",(0,n.jsx)(r.h3,{id:"advanced-mediatr-configuration",children:"Advanced MediatR configuration"}),"\n",(0,n.jsxs)(r.p,{children:["Access the underlying ",(0,n.jsx)(r.code,{children:"MediatRServiceConfiguration"})," to register handlers from additional assemblies or to set MediatR-specific options:"]}),"\n",(0,n.jsx)(r.pre,{children:(0,n.jsx)(r.code,{className:"language-csharp",children:"using System.Reflection;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithMediator(mediator =>\r\n {\r\n mediator.AddRequest();\r\n\r\n mediator.Configure(config =>\r\n {\r\n config.RegisterServicesFromAssemblies(typeof(Program).Assembly);\r\n });\r\n });\n"})}),"\n",(0,n.jsx)(r.h3,{id:"adding-pipeline-behaviors",children:"Adding pipeline behaviors"}),"\n",(0,n.jsx)(r.p,{children:"Pipeline behaviors run in registration order before and after every handler. RCommon ships three built-in behaviors:"}),"\n",(0,n.jsx)(r.pre,{children:(0,n.jsx)(r.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithMediator(mediator =>\r\n {\r\n mediator.AddRequest();\r\n\r\n // Adds LoggingRequestBehavior and LoggingRequestWithResponseBehavior\r\n mediator.AddLoggingToRequestPipeline();\r\n\r\n // Adds ValidatorBehavior and ValidatorBehaviorForMediatR (requires IValidationService)\r\n mediator.AddValidationToRequestPipeline();\r\n\r\n // Adds UnitOfWorkRequestBehavior and UnitOfWorkRequestWithResponseBehavior\r\n mediator.AddUnitOfWorkToRequestPipeline();\r\n });\n"})}),"\n",(0,n.jsx)(r.h2,{id:"usage",children:"Usage"}),"\n",(0,n.jsx)(r.h3,{id:"defining-requests",children:"Defining requests"}),"\n",(0,n.jsxs)(r.p,{children:["A fire-and-forget request implements ",(0,n.jsx)(r.code,{children:"IAppRequest"}),":"]}),"\n",(0,n.jsx)(r.pre,{children:(0,n.jsx)(r.code,{className:"language-csharp",children:"using RCommon.Mediator.Subscribers;\r\n\r\npublic class PlaceOrderRequest : IAppRequest\r\n{\r\n public PlaceOrderRequest(Guid customerId, IReadOnlyList lines)\r\n {\r\n CustomerId = customerId;\r\n Lines = lines;\r\n }\r\n\r\n public Guid CustomerId { get; }\r\n public IReadOnlyList Lines { get; }\r\n}\n"})}),"\n",(0,n.jsxs)(r.p,{children:["A request that returns a response implements ",(0,n.jsx)(r.code,{children:"IAppRequest"}),":"]}),"\n",(0,n.jsx)(r.pre,{children:(0,n.jsx)(r.code,{className:"language-csharp",children:"using RCommon.Mediator.Subscribers;\r\n\r\npublic class GetOrderRequest : IAppRequest\r\n{\r\n public GetOrderRequest(Guid orderId)\r\n {\r\n OrderId = orderId;\r\n }\r\n\r\n public Guid OrderId { get; }\r\n}\n"})}),"\n",(0,n.jsx)(r.h3,{id:"defining-request-handlers",children:"Defining request handlers"}),"\n",(0,n.jsxs)(r.p,{children:["Handle a fire-and-forget request by implementing ",(0,n.jsx)(r.code,{children:"IAppRequestHandler"}),":"]}),"\n",(0,n.jsx)(r.pre,{children:(0,n.jsx)(r.code,{className:"language-csharp",children:"using RCommon.Mediator.Subscribers;\r\n\r\npublic class PlaceOrderRequestHandler : IAppRequestHandler\r\n{\r\n private readonly IOrderRepository _orders;\r\n\r\n public PlaceOrderRequestHandler(IOrderRepository orders)\r\n {\r\n _orders = orders;\r\n }\r\n\r\n public async Task HandleAsync(PlaceOrderRequest request, CancellationToken cancellationToken = default)\r\n {\r\n var order = Order.Create(request.CustomerId, request.Lines);\r\n await _orders.AddAsync(order, cancellationToken);\r\n }\r\n}\n"})}),"\n",(0,n.jsxs)(r.p,{children:["Handle a request that returns a response by implementing ",(0,n.jsx)(r.code,{children:"IAppRequestHandler"}),":"]}),"\n",(0,n.jsx)(r.pre,{children:(0,n.jsx)(r.code,{className:"language-csharp",children:"using RCommon.Mediator.Subscribers;\r\n\r\npublic class GetOrderRequestHandler : IAppRequestHandler\r\n{\r\n private readonly IOrderRepository _orders;\r\n\r\n public GetOrderRequestHandler(IOrderRepository orders)\r\n {\r\n _orders = orders;\r\n }\r\n\r\n public async Task HandleAsync(GetOrderRequest request, CancellationToken cancellationToken = default)\r\n {\r\n var order = await _orders.GetByIdAsync(request.OrderId, cancellationToken);\r\n return new OrderDto { Id = order.Id, CustomerId = order.CustomerId };\r\n }\r\n}\n"})}),"\n",(0,n.jsx)(r.h3,{id:"defining-notifications",children:"Defining notifications"}),"\n",(0,n.jsxs)(r.p,{children:["Notifications are broadcast to all registered subscribers. Implement ",(0,n.jsx)(r.code,{children:"IAppNotification"})," on the notification class:"]}),"\n",(0,n.jsx)(r.pre,{children:(0,n.jsx)(r.code,{className:"language-csharp",children:"using RCommon.Mediator.Subscribers;\r\n\r\npublic class OrderPlacedNotification : IAppNotification\r\n{\r\n public OrderPlacedNotification(Guid orderId, DateTime placedAt)\r\n {\r\n OrderId = orderId;\r\n PlacedAt = placedAt;\r\n }\r\n\r\n public Guid OrderId { get; }\r\n public DateTime PlacedAt { get; }\r\n}\n"})}),"\n",(0,n.jsxs)(r.p,{children:["Subscribe by implementing ",(0,n.jsx)(r.code,{children:"ISubscriber"})," from ",(0,n.jsx)(r.code,{children:"RCommon.EventHandling.Subscribers"}),":"]}),"\n",(0,n.jsx)(r.pre,{children:(0,n.jsx)(r.code,{className:"language-csharp",children:"using RCommon.EventHandling.Subscribers;\r\n\r\npublic class OrderPlacedNotificationHandler : ISubscriber\r\n{\r\n public async Task HandleAsync(OrderPlacedNotification notification, CancellationToken cancellationToken = default)\r\n {\r\n // React to the notification \u2014 send email, update read model, etc.\r\n await Task.CompletedTask;\r\n }\r\n}\n"})}),"\n",(0,n.jsx)(r.h3,{id:"sending-and-publishing",children:"Sending and publishing"}),"\n",(0,n.jsxs)(r.p,{children:["Inject ",(0,n.jsx)(r.code,{children:"IMediatorService"})," and use ",(0,n.jsx)(r.code,{children:"Send"})," or ",(0,n.jsx)(r.code,{children:"Publish"}),":"]}),"\n",(0,n.jsx)(r.pre,{children:(0,n.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Mediator;\r\n\r\npublic class OrderController : ControllerBase\r\n{\r\n private readonly IMediatorService _mediator;\r\n\r\n public OrderController(IMediatorService mediator)\r\n {\r\n _mediator = mediator;\r\n }\r\n\r\n [HttpPost]\r\n public async Task PlaceOrder(PlaceOrderRequest request)\r\n {\r\n // Fire-and-forget: dispatches to a single handler\r\n await _mediator.Send(request);\r\n return Accepted();\r\n }\r\n\r\n [HttpGet("{id}")]\r\n public async Task GetOrder(Guid id)\r\n {\r\n // Request-response: dispatches to a single handler and returns a result\r\n return await _mediator.Send(new GetOrderRequest(id));\r\n }\r\n\r\n [HttpPost("{id}/events")]\r\n public async Task PublishOrderPlaced(Guid id)\r\n {\r\n // Notification: delivers to all registered subscribers\r\n await _mediator.Publish(new OrderPlacedNotification(id, DateTime.UtcNow));\r\n return NoContent();\r\n }\r\n}\n'})}),"\n",(0,n.jsx)(r.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,n.jsx)(r.h3,{id:"imediatorservice",children:(0,n.jsx)(r.code,{children:"IMediatorService"})}),"\n",(0,n.jsxs)(r.table,{children:[(0,n.jsx)(r.thead,{children:(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.th,{children:"Method"}),(0,n.jsx)(r.th,{children:"Description"})]})}),(0,n.jsxs)(r.tbody,{children:[(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"Send(TRequest, CancellationToken)"})}),(0,n.jsxs)(r.td,{children:["Dispatches a fire-and-forget request to a single handler. Wraps the request in ",(0,n.jsx)(r.code,{children:"MediatRRequest"})," internally."]})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"Send(TRequest, CancellationToken)"})}),(0,n.jsx)(r.td,{children:"Dispatches a request to a single handler and returns its response."})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"Publish(TNotification, CancellationToken)"})}),(0,n.jsxs)(r.td,{children:["Broadcasts a notification to all registered ",(0,n.jsx)(r.code,{children:"ISubscriber"})," implementations."]})]})]})]}),"\n",(0,n.jsxs)(r.h3,{id:"imediatrbuilder-registration-methods",children:[(0,n.jsx)(r.code,{children:"IMediatRBuilder"})," registration methods"]}),"\n",(0,n.jsxs)(r.table,{children:[(0,n.jsx)(r.thead,{children:(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.th,{children:"Method"}),(0,n.jsx)(r.th,{children:"Description"})]})}),(0,n.jsxs)(r.tbody,{children:[(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"AddRequest()"})}),(0,n.jsx)(r.td,{children:"Registers a fire-and-forget request and its handler."})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"AddRequest()"})}),(0,n.jsx)(r.td,{children:"Registers a request-response pair and its handler."})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"AddNotification()"})}),(0,n.jsx)(r.td,{children:"Registers a notification and its subscriber."})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"AddLoggingToRequestPipeline()"})}),(0,n.jsxs)(r.td,{children:["Registers ",(0,n.jsx)(r.code,{children:"LoggingRequestBehavior"})," and ",(0,n.jsx)(r.code,{children:"LoggingRequestWithResponseBehavior"}),"."]})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"AddValidationToRequestPipeline()"})}),(0,n.jsxs)(r.td,{children:["Registers ",(0,n.jsx)(r.code,{children:"ValidatorBehavior"}),", ",(0,n.jsx)(r.code,{children:"ValidatorBehaviorForMediatR"}),", and ",(0,n.jsx)(r.code,{children:"ValidationService"}),"."]})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"AddUnitOfWorkToRequestPipeline()"})}),(0,n.jsxs)(r.td,{children:["Registers ",(0,n.jsx)(r.code,{children:"UnitOfWorkRequestBehavior"})," and ",(0,n.jsx)(r.code,{children:"UnitOfWorkRequestWithResponseBehavior"}),"."]})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"Configure(Action)"})}),(0,n.jsxs)(r.td,{children:["Passes a configuration delegate directly to MediatR's ",(0,n.jsx)(r.code,{children:"AddMediatR"}),"."]})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"Configure(MediatRServiceConfiguration)"})}),(0,n.jsxs)(r.td,{children:["Passes a pre-built configuration instance to MediatR's ",(0,n.jsx)(r.code,{children:"AddMediatR"}),"."]})]})]})]}),"\n",(0,n.jsx)(r.h3,{id:"built-in-pipeline-behaviors",children:"Built-in pipeline behaviors"}),"\n",(0,n.jsxs)(r.table,{children:[(0,n.jsx)(r.thead,{children:(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.th,{children:"Behavior"}),(0,n.jsx)(r.th,{children:"Applies to"}),(0,n.jsx)(r.th,{children:"Description"})]})}),(0,n.jsxs)(r.tbody,{children:[(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"LoggingRequestBehavior"})}),(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"IRequest"})}),(0,n.jsx)(r.td,{children:"Logs request name and payload before and after handler execution."})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"LoggingRequestWithResponseBehavior"})}),(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"IRequest"})}),(0,n.jsx)(r.td,{children:"Same as above for requests that return a response."})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"ValidatorBehavior"})}),(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"IAppRequest"})}),(0,n.jsxs)(r.td,{children:["Validates ",(0,n.jsx)(r.code,{children:"IAppRequest"})," instances using ",(0,n.jsx)(r.code,{children:"IValidationService"}),". Throws on failure."]})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"ValidatorBehaviorForMediatR"})}),(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"IRequest"})}),(0,n.jsxs)(r.td,{children:["Validates raw MediatR ",(0,n.jsx)(r.code,{children:"IRequest"})," instances using ",(0,n.jsx)(r.code,{children:"IValidationService"}),"."]})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"UnitOfWorkRequestBehavior"})}),(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"IRequest"})}),(0,n.jsxs)(r.td,{children:["Wraps handler execution in a ",(0,n.jsx)(r.code,{children:"TransactionMode.Default"})," unit of work. Commits on success."]})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"UnitOfWorkRequestWithResponseBehavior"})}),(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"IRequest"})}),(0,n.jsx)(r.td,{children:"Same as above for requests that return a response."})]})]})]}),"\n",(0,n.jsx)(r.h3,{id:"marker-interfaces",children:"Marker interfaces"}),"\n",(0,n.jsxs)(r.table,{children:[(0,n.jsx)(r.thead,{children:(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.th,{children:"Interface"}),(0,n.jsx)(r.th,{children:"Package"}),(0,n.jsx)(r.th,{children:"Description"})]})}),(0,n.jsxs)(r.tbody,{children:[(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"IAppRequest"})}),(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"RCommon.Mediator"})}),(0,n.jsxs)(r.td,{children:["Marker for fire-and-forget requests dispatched via ",(0,n.jsx)(r.code,{children:"Send"}),"."]})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"IAppRequest"})}),(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"RCommon.Mediator"})}),(0,n.jsxs)(r.td,{children:["Marker for request-response requests dispatched via ",(0,n.jsx)(r.code,{children:"Send"}),"."]})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"IAppNotification"})}),(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"RCommon.Mediator"})}),(0,n.jsxs)(r.td,{children:["Marker for notifications dispatched via ",(0,n.jsx)(r.code,{children:"Publish"}),"."]})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"IAppRequestHandler"})}),(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"RCommon.Mediator"})}),(0,n.jsx)(r.td,{children:"Handler for fire-and-forget requests."})]}),(0,n.jsxs)(r.tr,{children:[(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"IAppRequestHandler"})}),(0,n.jsx)(r.td,{children:(0,n.jsx)(r.code,{children:"RCommon.Mediator"})}),(0,n.jsx)(r.td,{children:"Handler for request-response requests."})]})]})]})]})}function u(e={}){const{wrapper:r}={...(0,d.R)(),...e.components};return r?(0,n.jsx)(r,{...e,children:(0,n.jsx)(h,{...e})}):h(e)}},75783(e,r,i){i.d(r,{A:()=>l});var n=i(30758);const d="container_xjrG",s="label_Y4p8",t="commandRow_FY5I",o="command_m7Qs",a="copyButton_u1GK";var c=i(86070);function l({packageName:e,version:r}){const[i,l]=(0,n.useState)(!1),h=r?`dotnet add package ${e} --version ${r}`:`dotnet add package ${e}`;return(0,c.jsxs)("div",{className:d,children:[(0,c.jsx)("div",{className:s,children:"NuGet Package"}),(0,c.jsxs)("div",{className:t,children:[(0,c.jsx)("code",{className:o,children:h}),(0,c.jsx)("button",{className:a,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:i?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,r,i){i.d(r,{R:()=>t,x:()=>o});var n=i(30758);const d={},s=n.createContext(d);function t(e){const r=n.useContext(s);return n.useMemo(function(){return"function"==typeof e?e(r):{...r,...e}},[r,e])}function o(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(d):e.components||d:t(e.components),n.createElement(s.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/10134e3e.d169c845.js b/website/build/assets/js/10134e3e.d169c845.js new file mode 100644 index 00000000..075e8b4a --- /dev/null +++ b/website/build/assets/js/10134e3e.d169c845.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[9865],{46984(e,i,n){n.r(i),n.d(i,{assets:()=>c,contentTitle:()=>o,default:()=>u,frontMatter:()=>t,metadata:()=>a,toc:()=>l});var r=n(86070),d=n(81753),s=n(75783);const t={title:"MediatR",sidebar_position:4,description:"Wire MediatR into RCommon's mediator abstraction for request-response and fan-out notifications, with built-in logging, validation, and unit-of-work pipeline behaviors."},o="MediatR",a={id:"cqrs-mediator/mediatr",title:"MediatR",description:"Wire MediatR into RCommon's mediator abstraction for request-response and fan-out notifications, with built-in logging, validation, and unit-of-work pipeline behaviors.",source:"@site/docs/cqrs-mediator/mediatr.mdx",sourceDirName:"cqrs-mediator",slug:"/cqrs-mediator/mediatr",permalink:"/docs/next/cqrs-mediator/mediatr",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/cqrs-mediator/mediatr.mdx",tags:[],version:"current",sidebarPosition:4,frontMatter:{title:"MediatR",sidebar_position:4,description:"Wire MediatR into RCommon's mediator abstraction for request-response and fan-out notifications, with built-in logging, validation, and unit-of-work pipeline behaviors."},sidebar:"docsSidebar",previous:{title:"Queries & Handlers",permalink:"/docs/next/cqrs-mediator/queries-handlers"},next:{title:"Wolverine",permalink:"/docs/next/cqrs-mediator/wolverine"}},c={},l=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Advanced MediatR configuration",id:"advanced-mediatr-configuration",level:3},{value:"Adding pipeline behaviors",id:"adding-pipeline-behaviors",level:3},{value:"Usage",id:"usage",level:2},{value:"Defining requests",id:"defining-requests",level:3},{value:"Defining request handlers",id:"defining-request-handlers",level:3},{value:"Defining notifications",id:"defining-notifications",level:3},{value:"Sending and publishing",id:"sending-and-publishing",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"IMediatorService",id:"imediatorservice",level:3},{value:"IMediatRBuilder registration methods",id:"imediatrbuilder-registration-methods",level:3},{value:"Built-in pipeline behaviors",id:"built-in-pipeline-behaviors",level:3},{value:"Marker interfaces",id:"marker-interfaces",level:3}];function h(e){const i={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(i.h1,{id:"mediatr",children:"MediatR"}),"\n",(0,r.jsx)(i.h2,{id:"overview",children:"Overview"}),"\n",(0,r.jsxs)(i.p,{children:[(0,r.jsx)(i.code,{children:"RCommon.MediatR"})," adapts the ",(0,r.jsx)(i.a,{href:"https://github.com/jbogard/MediatR",children:"MediatR"})," library to RCommon's mediator abstraction. It provides:"]}),"\n",(0,r.jsxs)(i.ul,{children:["\n",(0,r.jsxs)(i.li,{children:[(0,r.jsx)(i.code,{children:"MediatRAdapter"})," \u2014 an ",(0,r.jsx)(i.code,{children:"IMediatorAdapter"})," implementation that delegates ",(0,r.jsx)(i.code,{children:"Send"})," and ",(0,r.jsx)(i.code,{children:"Publish"})," calls to MediatR's ",(0,r.jsx)(i.code,{children:"IMediator"}),"."]}),"\n",(0,r.jsxs)(i.li,{children:[(0,r.jsx)(i.code,{children:"MediatRBuilder"})," \u2014 an ",(0,r.jsx)(i.code,{children:"IMediatRBuilder"})," that wires MediatR into the RCommon service container."]}),"\n",(0,r.jsx)(i.li,{children:"Pre-built pipeline behaviors for logging, validation, and unit-of-work."}),"\n",(0,r.jsxs)(i.li,{children:["Wrapper types (",(0,r.jsx)(i.code,{children:"MediatRRequest"}),", ",(0,r.jsx)(i.code,{children:"MediatRNotification"}),") that bridge RCommon message contracts to MediatR's internal types."]}),"\n"]}),"\n",(0,r.jsxs)(i.p,{children:["Application code depends only on ",(0,r.jsx)(i.code,{children:"IMediatorService"})," from ",(0,r.jsx)(i.code,{children:"RCommon.Mediator"}),". The MediatR implementation is an infrastructure detail registered at startup."]}),"\n",(0,r.jsx)(i.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(s.A,{packageName:"RCommon.MediatR"}),"\n",(0,r.jsxs)(i.p,{children:["This package depends on ",(0,r.jsx)(i.code,{children:"RCommon.Mediator"}),", ",(0,r.jsx)(i.code,{children:"RCommon.ApplicationServices"}),", ",(0,r.jsx)(i.code,{children:"RCommon.Core"}),", ",(0,r.jsx)(i.code,{children:"RCommon.Persistence"}),", and the ",(0,r.jsx)(i.code,{children:"MediatR"})," NuGet package. All are pulled in transitively."]}),"\n",(0,r.jsx)(i.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsxs)(i.p,{children:["Register the MediatR provider inside ",(0,r.jsx)(i.code,{children:"AddRCommon()"})," using ",(0,r.jsx)(i.code,{children:"WithMediator"}),". The configuration delegate is where you register your requests, notifications, and pipeline behaviors."]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"using RCommon;\nusing RCommon.Mediator.MediatR;\n\nbuilder.Services.AddRCommon()\n .WithMediator(mediator =>\n {\n // Register a fire-and-forget request (no response)\n mediator.AddRequest();\n\n // Register a request that returns a response\n mediator.AddRequest();\n\n // Register a notification (fan-out to all subscribers)\n mediator.AddNotification();\n });\n"})}),"\n",(0,r.jsx)(i.admonition,{title:"Modular composition",type:"tip",children:(0,r.jsxs)(i.p,{children:[(0,r.jsx)(i.code,{children:"WithMediator"})," is cache-aware. When multiple modules call it, the cached ",(0,r.jsx)(i.code,{children:"MediatRBuilder"})," is reused and each module's configuration delegate runs against the same instance \u2014 ",(0,r.jsx)(i.code,{children:"AddRequest"}),", ",(0,r.jsx)(i.code,{children:"AddNotification"}),", ",(0,r.jsx)(i.code,{children:"AddLoggingToRequestPipeline"}),", ",(0,r.jsx)(i.code,{children:"AddUnitOfWorkToRequestPipeline"}),", and ",(0,r.jsx)(i.code,{children:"Configure(...)"})," registrations from every module accumulate on one builder. This lets each module register its own command/query handlers without coordinating with the others. See ",(0,r.jsx)(i.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,r.jsx)(i.h3,{id:"advanced-mediatr-configuration",children:"Advanced MediatR configuration"}),"\n",(0,r.jsxs)(i.p,{children:["Access the underlying ",(0,r.jsx)(i.code,{children:"MediatRServiceConfiguration"})," to register handlers from additional assemblies or to set MediatR-specific options:"]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"using System.Reflection;\n\nbuilder.Services.AddRCommon()\n .WithMediator(mediator =>\n {\n mediator.AddRequest();\n\n mediator.Configure(config =>\n {\n config.RegisterServicesFromAssemblies(typeof(Program).Assembly);\n });\n });\n"})}),"\n",(0,r.jsx)(i.h3,{id:"adding-pipeline-behaviors",children:"Adding pipeline behaviors"}),"\n",(0,r.jsx)(i.p,{children:"Pipeline behaviors run in registration order before and after every handler. RCommon ships three built-in behaviors:"}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithMediator(mediator =>\n {\n mediator.AddRequest();\n\n // Adds LoggingRequestBehavior and LoggingRequestWithResponseBehavior\n mediator.AddLoggingToRequestPipeline();\n\n // Adds ValidatorBehavior and ValidatorBehaviorForMediatR (requires IValidationService)\n mediator.AddValidationToRequestPipeline();\n\n // Adds UnitOfWorkRequestBehavior and UnitOfWorkRequestWithResponseBehavior\n mediator.AddUnitOfWorkToRequestPipeline();\n });\n"})}),"\n",(0,r.jsx)(i.h2,{id:"usage",children:"Usage"}),"\n",(0,r.jsx)(i.h3,{id:"defining-requests",children:"Defining requests"}),"\n",(0,r.jsxs)(i.p,{children:["A fire-and-forget request implements ",(0,r.jsx)(i.code,{children:"IAppRequest"}),":"]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"using RCommon.Mediator.Subscribers;\n\npublic class PlaceOrderRequest : IAppRequest\n{\n public PlaceOrderRequest(Guid customerId, IReadOnlyList lines)\n {\n CustomerId = customerId;\n Lines = lines;\n }\n\n public Guid CustomerId { get; }\n public IReadOnlyList Lines { get; }\n}\n"})}),"\n",(0,r.jsxs)(i.p,{children:["A request that returns a response implements ",(0,r.jsx)(i.code,{children:"IAppRequest"}),":"]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"using RCommon.Mediator.Subscribers;\n\npublic class GetOrderRequest : IAppRequest\n{\n public GetOrderRequest(Guid orderId)\n {\n OrderId = orderId;\n }\n\n public Guid OrderId { get; }\n}\n"})}),"\n",(0,r.jsx)(i.h3,{id:"defining-request-handlers",children:"Defining request handlers"}),"\n",(0,r.jsxs)(i.p,{children:["Handle a fire-and-forget request by implementing ",(0,r.jsx)(i.code,{children:"IAppRequestHandler"}),":"]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"using RCommon.Mediator.Subscribers;\n\npublic class PlaceOrderRequestHandler : IAppRequestHandler\n{\n private readonly IOrderRepository _orders;\n\n public PlaceOrderRequestHandler(IOrderRepository orders)\n {\n _orders = orders;\n }\n\n public async Task HandleAsync(PlaceOrderRequest request, CancellationToken cancellationToken = default)\n {\n var order = Order.Create(request.CustomerId, request.Lines);\n await _orders.AddAsync(order, cancellationToken);\n }\n}\n"})}),"\n",(0,r.jsxs)(i.p,{children:["Handle a request that returns a response by implementing ",(0,r.jsx)(i.code,{children:"IAppRequestHandler"}),":"]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"using RCommon.Mediator.Subscribers;\n\npublic class GetOrderRequestHandler : IAppRequestHandler\n{\n private readonly IOrderRepository _orders;\n\n public GetOrderRequestHandler(IOrderRepository orders)\n {\n _orders = orders;\n }\n\n public async Task HandleAsync(GetOrderRequest request, CancellationToken cancellationToken = default)\n {\n var order = await _orders.GetByIdAsync(request.OrderId, cancellationToken);\n return new OrderDto { Id = order.Id, CustomerId = order.CustomerId };\n }\n}\n"})}),"\n",(0,r.jsx)(i.h3,{id:"defining-notifications",children:"Defining notifications"}),"\n",(0,r.jsxs)(i.p,{children:["Notifications are broadcast to all registered subscribers. Implement ",(0,r.jsx)(i.code,{children:"IAppNotification"})," on the notification class:"]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"using RCommon.Mediator.Subscribers;\n\npublic class OrderPlacedNotification : IAppNotification\n{\n public OrderPlacedNotification(Guid orderId, DateTime placedAt)\n {\n OrderId = orderId;\n PlacedAt = placedAt;\n }\n\n public Guid OrderId { get; }\n public DateTime PlacedAt { get; }\n}\n"})}),"\n",(0,r.jsxs)(i.p,{children:["Subscribe by implementing ",(0,r.jsx)(i.code,{children:"ISubscriber"})," from ",(0,r.jsx)(i.code,{children:"RCommon.EventHandling.Subscribers"}),":"]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:"using RCommon.EventHandling.Subscribers;\n\npublic class OrderPlacedNotificationHandler : ISubscriber\n{\n public async Task HandleAsync(OrderPlacedNotification notification, CancellationToken cancellationToken = default)\n {\n // React to the notification \u2014 send email, update read model, etc.\n await Task.CompletedTask;\n }\n}\n"})}),"\n",(0,r.jsx)(i.h3,{id:"sending-and-publishing",children:"Sending and publishing"}),"\n",(0,r.jsxs)(i.p,{children:["Inject ",(0,r.jsx)(i.code,{children:"IMediatorService"})," and use ",(0,r.jsx)(i.code,{children:"Send"})," or ",(0,r.jsx)(i.code,{children:"Publish"}),":"]}),"\n",(0,r.jsx)(i.pre,{children:(0,r.jsx)(i.code,{className:"language-csharp",children:'using RCommon.Mediator;\n\npublic class OrderController : ControllerBase\n{\n private readonly IMediatorService _mediator;\n\n public OrderController(IMediatorService mediator)\n {\n _mediator = mediator;\n }\n\n [HttpPost]\n public async Task PlaceOrder(PlaceOrderRequest request)\n {\n // Fire-and-forget: dispatches to a single handler\n await _mediator.Send(request);\n return Accepted();\n }\n\n [HttpGet("{id}")]\n public async Task GetOrder(Guid id)\n {\n // Request-response: dispatches to a single handler and returns a result\n return await _mediator.Send(new GetOrderRequest(id));\n }\n\n [HttpPost("{id}/events")]\n public async Task PublishOrderPlaced(Guid id)\n {\n // Notification: delivers to all registered subscribers\n await _mediator.Publish(new OrderPlacedNotification(id, DateTime.UtcNow));\n return NoContent();\n }\n}\n'})}),"\n",(0,r.jsx)(i.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,r.jsx)(i.h3,{id:"imediatorservice",children:(0,r.jsx)(i.code,{children:"IMediatorService"})}),"\n",(0,r.jsxs)(i.table,{children:[(0,r.jsx)(i.thead,{children:(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.th,{children:"Method"}),(0,r.jsx)(i.th,{children:"Description"})]})}),(0,r.jsxs)(i.tbody,{children:[(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"Send(TRequest, CancellationToken)"})}),(0,r.jsxs)(i.td,{children:["Dispatches a fire-and-forget request to a single handler. Wraps the request in ",(0,r.jsx)(i.code,{children:"MediatRRequest"})," internally."]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"Send(TRequest, CancellationToken)"})}),(0,r.jsx)(i.td,{children:"Dispatches a request to a single handler and returns its response."})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"Publish(TNotification, CancellationToken)"})}),(0,r.jsxs)(i.td,{children:["Broadcasts a notification to all registered ",(0,r.jsx)(i.code,{children:"ISubscriber"})," implementations."]})]})]})]}),"\n",(0,r.jsxs)(i.h3,{id:"imediatrbuilder-registration-methods",children:[(0,r.jsx)(i.code,{children:"IMediatRBuilder"})," registration methods"]}),"\n",(0,r.jsxs)(i.table,{children:[(0,r.jsx)(i.thead,{children:(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.th,{children:"Method"}),(0,r.jsx)(i.th,{children:"Description"})]})}),(0,r.jsxs)(i.tbody,{children:[(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"AddRequest()"})}),(0,r.jsx)(i.td,{children:"Registers a fire-and-forget request and its handler."})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"AddRequest()"})}),(0,r.jsx)(i.td,{children:"Registers a request-response pair and its handler."})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"AddNotification()"})}),(0,r.jsx)(i.td,{children:"Registers a notification and its subscriber."})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"AddLoggingToRequestPipeline()"})}),(0,r.jsxs)(i.td,{children:["Registers ",(0,r.jsx)(i.code,{children:"LoggingRequestBehavior"})," and ",(0,r.jsx)(i.code,{children:"LoggingRequestWithResponseBehavior"}),"."]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"AddValidationToRequestPipeline()"})}),(0,r.jsxs)(i.td,{children:["Registers ",(0,r.jsx)(i.code,{children:"ValidatorBehavior"}),", ",(0,r.jsx)(i.code,{children:"ValidatorBehaviorForMediatR"}),", and ",(0,r.jsx)(i.code,{children:"ValidationService"}),"."]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"AddUnitOfWorkToRequestPipeline()"})}),(0,r.jsxs)(i.td,{children:["Registers ",(0,r.jsx)(i.code,{children:"UnitOfWorkRequestBehavior"})," and ",(0,r.jsx)(i.code,{children:"UnitOfWorkRequestWithResponseBehavior"}),"."]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"Configure(Action)"})}),(0,r.jsxs)(i.td,{children:["Passes a configuration delegate directly to MediatR's ",(0,r.jsx)(i.code,{children:"AddMediatR"}),"."]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"Configure(MediatRServiceConfiguration)"})}),(0,r.jsxs)(i.td,{children:["Passes a pre-built configuration instance to MediatR's ",(0,r.jsx)(i.code,{children:"AddMediatR"}),"."]})]})]})]}),"\n",(0,r.jsx)(i.h3,{id:"built-in-pipeline-behaviors",children:"Built-in pipeline behaviors"}),"\n",(0,r.jsxs)(i.table,{children:[(0,r.jsx)(i.thead,{children:(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.th,{children:"Behavior"}),(0,r.jsx)(i.th,{children:"Applies to"}),(0,r.jsx)(i.th,{children:"Description"})]})}),(0,r.jsxs)(i.tbody,{children:[(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"LoggingRequestBehavior"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"IRequest"})}),(0,r.jsx)(i.td,{children:"Logs request name and payload before and after handler execution."})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"LoggingRequestWithResponseBehavior"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"IRequest"})}),(0,r.jsx)(i.td,{children:"Same as above for requests that return a response."})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"ValidatorBehavior"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"IAppRequest"})}),(0,r.jsxs)(i.td,{children:["Validates ",(0,r.jsx)(i.code,{children:"IAppRequest"})," instances using ",(0,r.jsx)(i.code,{children:"IValidationService"}),". Throws on failure."]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"ValidatorBehaviorForMediatR"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"IRequest"})}),(0,r.jsxs)(i.td,{children:["Validates raw MediatR ",(0,r.jsx)(i.code,{children:"IRequest"})," instances using ",(0,r.jsx)(i.code,{children:"IValidationService"}),"."]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"UnitOfWorkRequestBehavior"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"IRequest"})}),(0,r.jsxs)(i.td,{children:["Wraps handler execution in a ",(0,r.jsx)(i.code,{children:"TransactionMode.Default"})," unit of work. Commits on success."]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"UnitOfWorkRequestWithResponseBehavior"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"IRequest"})}),(0,r.jsx)(i.td,{children:"Same as above for requests that return a response."})]})]})]}),"\n",(0,r.jsx)(i.h3,{id:"marker-interfaces",children:"Marker interfaces"}),"\n",(0,r.jsxs)(i.table,{children:[(0,r.jsx)(i.thead,{children:(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.th,{children:"Interface"}),(0,r.jsx)(i.th,{children:"Package"}),(0,r.jsx)(i.th,{children:"Description"})]})}),(0,r.jsxs)(i.tbody,{children:[(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"IAppRequest"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.Mediator"})}),(0,r.jsxs)(i.td,{children:["Marker for fire-and-forget requests dispatched via ",(0,r.jsx)(i.code,{children:"Send"}),"."]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"IAppRequest"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.Mediator"})}),(0,r.jsxs)(i.td,{children:["Marker for request-response requests dispatched via ",(0,r.jsx)(i.code,{children:"Send"}),"."]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"IAppNotification"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.Mediator"})}),(0,r.jsxs)(i.td,{children:["Marker for notifications dispatched via ",(0,r.jsx)(i.code,{children:"Publish"}),"."]})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"IAppRequestHandler"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.Mediator"})}),(0,r.jsx)(i.td,{children:"Handler for fire-and-forget requests."})]}),(0,r.jsxs)(i.tr,{children:[(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"IAppRequestHandler"})}),(0,r.jsx)(i.td,{children:(0,r.jsx)(i.code,{children:"RCommon.Mediator"})}),(0,r.jsx)(i.td,{children:"Handler for request-response requests."})]})]})]})]})}function u(e={}){const{wrapper:i}={...(0,d.R)(),...e.components};return i?(0,r.jsx)(i,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},75783(e,i,n){n.d(i,{A:()=>l});var r=n(30758);const d="container_xjrG",s="label_Y4p8",t="commandRow_FY5I",o="command_m7Qs",a="copyButton_u1GK";var c=n(86070);function l({packageName:e,version:i}){const[n,l]=(0,r.useState)(!1),h=i?`dotnet add package ${e} --version ${i}`:`dotnet add package ${e}`;return(0,c.jsxs)("div",{className:d,children:[(0,c.jsx)("div",{className:s,children:"NuGet Package"}),(0,c.jsxs)("div",{className:t,children:[(0,c.jsx)("code",{className:o,children:h}),(0,c.jsx)("button",{className:a,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:n?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,i,n){n.d(i,{R:()=>t,x:()=>o});var r=n(30758);const d={},s=r.createContext(d);function t(e){const i=r.useContext(s);return r.useMemo(function(){return"function"==typeof e?e(i):{...i,...e}},[i,e])}function o(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(d):e.components||d:t(e.components),r.createElement(s.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/114ecb34.2b676dcc.js b/website/build/assets/js/114ecb34.2b676dcc.js new file mode 100644 index 00000000..56326977 --- /dev/null +++ b/website/build/assets/js/114ecb34.2b676dcc.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[4006],{49637(e,t,n){n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>h,frontMatter:()=>r,metadata:()=>o,toc:()=>d});var i=n(86070),a=n(81753);const r={title:"Overview",sidebar_position:1,description:"Provider-agnostic finite state machine abstraction for .NET using C# enums, with Stateless and MassTransit adapter options for workflow and lifecycle modeling."},s="State Machines Overview",o={id:"state-machines/overview",title:"Overview",description:"Provider-agnostic finite state machine abstraction for .NET using C# enums, with Stateless and MassTransit adapter options for workflow and lifecycle modeling.",source:"@site/docs/state-machines/overview.mdx",sourceDirName:"state-machines",slug:"/state-machines/overview",permalink:"/docs/next/state-machines/overview",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/state-machines/overview.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Overview",sidebar_position:1,description:"Provider-agnostic finite state machine abstraction for .NET using C# enums, with Stateless and MassTransit adapter options for workflow and lifecycle modeling."},sidebar:"docsSidebar",previous:{title:"State Machines",permalink:"/docs/next/category/state-machines"},next:{title:"Stateless",permalink:"/docs/next/state-machines/stateless"}},c={},d=[{value:"The state machine pattern",id:"the-state-machine-pattern",level:2},{value:"Core abstractions",id:"core-abstractions",level:2},{value:"IStateMachineConfigurator<TState, TTrigger>",id:"istatemachineconfiguratortstate-ttrigger",level:3},{value:"IStateConfigurator<TState, TTrigger>",id:"istateconfiguratortstate-ttrigger",level:3},{value:"IStateMachine<TState, TTrigger>",id:"istatemachinetstate-ttrigger",level:3},{value:"Stateless vs MassTransit adapter",id:"stateless-vs-masstransit-adapter",level:2},{value:"Choosing an adapter",id:"choosing-an-adapter",level:2},{value:"Section contents",id:"section-contents",level:2}];function l(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,a.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.h1,{id:"state-machines-overview",children:"State Machines Overview"}),"\n",(0,i.jsxs)(t.p,{children:["RCommon provides a provider-agnostic state machine abstraction that lets you define finite state machines (FSMs) using plain C# enums for states and triggers. The abstraction is backed by pluggable adapters, currently the ",(0,i.jsx)(t.code,{children:"Stateless"})," library and a lightweight dictionary-based implementation included in ",(0,i.jsx)(t.code,{children:"RCommon.MassTransit.StateMachines"}),"."]}),"\n",(0,i.jsx)(t.h2,{id:"the-state-machine-pattern",children:"The state machine pattern"}),"\n",(0,i.jsx)(t.p,{children:"A finite state machine models a process that is always in exactly one of a finite number of states. Transitions between states are driven by triggers (also called events or signals). Each transition can be unconditional or guarded by a condition. States can have entry and exit actions that fire async side effects when the machine moves in or out of them."}),"\n",(0,i.jsx)(t.p,{children:"Common use cases include:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Order lifecycle management (Pending \u2192 Approved \u2192 Shipped \u2192 Delivered)"}),"\n",(0,i.jsx)(t.li,{children:"Workflow approval processes (Draft \u2192 Submitted \u2192 Under Review \u2192 Approved / Rejected)"}),"\n",(0,i.jsx)(t.li,{children:"Connection state tracking (Connecting \u2192 Connected \u2192 Disconnecting \u2192 Disconnected)"}),"\n",(0,i.jsx)(t.li,{children:"Document publishing pipelines (Draft \u2192 Review \u2192 Published / Archived)"}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"core-abstractions",children:"Core abstractions"}),"\n",(0,i.jsx)(t.h3,{id:"istatemachineconfiguratortstate-ttrigger",children:"IStateMachineConfigurator"}),"\n",(0,i.jsxs)(t.p,{children:["The entry point for defining a state machine. Call ",(0,i.jsx)(t.code,{children:"ForState"})," once per state to configure its transitions, then call ",(0,i.jsx)(t.code,{children:"Build"})," to create a running machine instance:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:"public interface IStateMachineConfigurator\n where TState : struct, Enum\n where TTrigger : struct, Enum\n{\n IStateConfigurator ForState(TState state);\n IStateMachine Build(TState initialState);\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"A single configurator instance can be used to build multiple independent machine instances with different initial states, all sharing the same transition configuration."}),"\n",(0,i.jsx)(t.h3,{id:"istateconfiguratortstate-ttrigger",children:"IStateConfigurator"}),"\n",(0,i.jsxs)(t.p,{children:["Returned by ",(0,i.jsx)(t.code,{children:"ForState"}),", used to define transitions and actions for a specific state:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:"public interface IStateConfigurator\n where TState : struct, Enum\n where TTrigger : struct, Enum\n{\n IStateConfigurator Permit(TTrigger trigger, TState destinationState);\n IStateConfigurator PermitIf(TTrigger trigger, TState destinationState, Func guard);\n IStateConfigurator OnEntry(Func action);\n IStateConfigurator OnExit(Func action);\n}\n"})}),"\n",(0,i.jsx)(t.h3,{id:"istatemachinetstate-ttrigger",children:"IStateMachine"}),"\n",(0,i.jsx)(t.p,{children:"The running state machine instance. Inspect current state, check permitted triggers, and fire them:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:"public interface IStateMachine\n where TState : struct, Enum\n where TTrigger : struct, Enum\n{\n TState CurrentState { get; }\n bool CanFire(TTrigger trigger);\n IEnumerable PermittedTriggers { get; }\n Task FireAsync(TTrigger trigger, CancellationToken cancellationToken = default);\n Task FireAsync(TTrigger trigger, TData data, CancellationToken cancellationToken = default);\n}\n"})}),"\n",(0,i.jsx)(t.h2,{id:"stateless-vs-masstransit-adapter",children:"Stateless vs MassTransit adapter"}),"\n",(0,i.jsxs)(t.p,{children:["RCommon ships two adapters for the state machine abstraction. Both register as ",(0,i.jsx)(t.code,{children:"IStateMachineConfigurator"})," and produce ",(0,i.jsx)(t.code,{children:"IStateMachine"})," instances:"]}),"\n",(0,i.jsxs)(t.table,{children:[(0,i.jsx)(t.thead,{children:(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.th,{children:"Aspect"}),(0,i.jsx)(t.th,{children:(0,i.jsx)(t.code,{children:"RCommon.Stateless"})}),(0,i.jsx)(t.th,{children:(0,i.jsx)(t.code,{children:"RCommon.MassTransit.StateMachines"})})]})}),(0,i.jsxs)(t.tbody,{children:[(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:"Backing library"}),(0,i.jsxs)(t.td,{children:[(0,i.jsx)(t.code,{children:"Stateless"})," NuGet package"]}),(0,i.jsx)(t.td,{children:"Custom dictionary-based FSM (no extra dependency)"})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:"Parameterized triggers"}),(0,i.jsxs)(t.td,{children:["Fully supported via Stateless ",(0,i.jsx)(t.code,{children:"SetTriggerParameters"})]}),(0,i.jsx)(t.td,{children:"Accepted but data parameter is ignored"})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:"Configuration model"}),(0,i.jsxs)(t.td,{children:["Deferred: actions recorded and replayed on each ",(0,i.jsx)(t.code,{children:"Build"})," call"]}),(0,i.jsxs)(t.td,{children:["Shared dictionary cloned per ",(0,i.jsx)(t.code,{children:"Build"})," call"]})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:"Registration"}),(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"WithStatelessStateMachine()"})}),(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"WithMassTransitStateMachine()"})})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:"Suitable for"}),(0,i.jsx)(t.td,{children:"Most applications; rich guard and parameterized trigger support"}),(0,i.jsx)(t.td,{children:"Simple state tracking in messaging-heavy contexts"})]})]})]}),"\n",(0,i.jsxs)(t.p,{children:["Use ",(0,i.jsx)(t.code,{children:"RCommon.Stateless"})," unless you have a specific reason to prefer the dictionary-based adapter."]}),"\n",(0,i.jsx)(t.h2,{id:"choosing-an-adapter",children:"Choosing an adapter"}),"\n",(0,i.jsxs)(t.p,{children:["Choose ",(0,i.jsx)(t.code,{children:"RCommon.Stateless"})," when:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Your state machine needs parameterized triggers with data passed to entry actions."}),"\n",(0,i.jsx)(t.li,{children:"You want the full feature set of the Stateless library (sub-states, trigger parameters, dot-graph export)."}),"\n",(0,i.jsx)(t.li,{children:"You prefer a well-established open-source library as the engine."}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["Choose ",(0,i.jsx)(t.code,{children:"RCommon.MassTransit.StateMachines"})," when:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["You are already using ",(0,i.jsx)(t.code,{children:"RCommon.MassTransit"})," and want to avoid an additional dependency."]}),"\n",(0,i.jsx)(t.li,{children:"Your state machine only uses simple unconditional transitions and guarded transitions with no trigger data."}),"\n"]}),"\n",(0,i.jsx)(t.admonition,{title:"Modular composition",type:"tip",children:(0,i.jsxs)(t.p,{children:["Both ",(0,i.jsx)(t.code,{children:"WithStatelessStateMachine()"})," and ",(0,i.jsx)(t.code,{children:"WithMassTransitStateMachine()"})," are ",(0,i.jsx)(t.code,{children:"TryAdd"}),"-hardened \u2014 repeat calls from multiple modules are idempotent and the open-generic ",(0,i.jsx)(t.code,{children:"IStateMachineConfigurator<,>"})," registration is added exactly once. Modules can opt into state-machine support independently. Registering both adapters in the same process is not recommended (the second registration is ignored by ",(0,i.jsx)(t.code,{children:"TryAdd"}),"); pick one per process. See ",(0,i.jsx)(t.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,i.jsx)(t.h2,{id:"section-contents",children:"Section contents"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.a,{href:"/docs/next/state-machines/stateless",children:"Stateless"})," \u2014 Stateless library integration, defining states/triggers, full configuration reference"]}),"\n"]})]})}function h(e={}){const{wrapper:t}={...(0,a.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(l,{...e})}):l(e)}},81753(e,t,n){n.d(t,{R:()=>s,x:()=>o});var i=n(30758);const a={},r=i.createContext(a);function s(e){const t=i.useContext(r);return i.useMemo(function(){return"function"==typeof e?e(t):{...t,...e}},[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),i.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/114ecb34.4bf295b8.js b/website/build/assets/js/114ecb34.4bf295b8.js deleted file mode 100644 index 8d5dc723..00000000 --- a/website/build/assets/js/114ecb34.4bf295b8.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[4006],{49637(e,t,n){n.r(t),n.d(t,{assets:()=>o,contentTitle:()=>s,default:()=>h,frontMatter:()=>r,metadata:()=>c,toc:()=>d});var i=n(86070),a=n(81753);const r={title:"Overview",sidebar_position:1,description:"Provider-agnostic finite state machine abstraction for .NET using C# enums, with Stateless and MassTransit adapter options for workflow and lifecycle modeling."},s="State Machines Overview",c={id:"state-machines/overview",title:"Overview",description:"Provider-agnostic finite state machine abstraction for .NET using C# enums, with Stateless and MassTransit adapter options for workflow and lifecycle modeling.",source:"@site/docs/state-machines/overview.mdx",sourceDirName:"state-machines",slug:"/state-machines/overview",permalink:"/docs/next/state-machines/overview",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/state-machines/overview.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Overview",sidebar_position:1,description:"Provider-agnostic finite state machine abstraction for .NET using C# enums, with Stateless and MassTransit adapter options for workflow and lifecycle modeling."},sidebar:"docsSidebar",previous:{title:"State Machines",permalink:"/docs/next/category/state-machines"},next:{title:"Stateless",permalink:"/docs/next/state-machines/stateless"}},o={},d=[{value:"The state machine pattern",id:"the-state-machine-pattern",level:2},{value:"Core abstractions",id:"core-abstractions",level:2},{value:"IStateMachineConfigurator<TState, TTrigger>",id:"istatemachineconfiguratortstate-ttrigger",level:3},{value:"IStateConfigurator<TState, TTrigger>",id:"istateconfiguratortstate-ttrigger",level:3},{value:"IStateMachine<TState, TTrigger>",id:"istatemachinetstate-ttrigger",level:3},{value:"Stateless vs MassTransit adapter",id:"stateless-vs-masstransit-adapter",level:2},{value:"Choosing an adapter",id:"choosing-an-adapter",level:2},{value:"Section contents",id:"section-contents",level:2}];function l(e){const t={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,a.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.h1,{id:"state-machines-overview",children:"State Machines Overview"}),"\n",(0,i.jsxs)(t.p,{children:["RCommon provides a provider-agnostic state machine abstraction that lets you define finite state machines (FSMs) using plain C# enums for states and triggers. The abstraction is backed by pluggable adapters, currently the ",(0,i.jsx)(t.code,{children:"Stateless"})," library and a lightweight dictionary-based implementation included in ",(0,i.jsx)(t.code,{children:"RCommon.MassTransit.StateMachines"}),"."]}),"\n",(0,i.jsx)(t.h2,{id:"the-state-machine-pattern",children:"The state machine pattern"}),"\n",(0,i.jsx)(t.p,{children:"A finite state machine models a process that is always in exactly one of a finite number of states. Transitions between states are driven by triggers (also called events or signals). Each transition can be unconditional or guarded by a condition. States can have entry and exit actions that fire async side effects when the machine moves in or out of them."}),"\n",(0,i.jsx)(t.p,{children:"Common use cases include:"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Order lifecycle management (Pending \u2192 Approved \u2192 Shipped \u2192 Delivered)"}),"\n",(0,i.jsx)(t.li,{children:"Workflow approval processes (Draft \u2192 Submitted \u2192 Under Review \u2192 Approved / Rejected)"}),"\n",(0,i.jsx)(t.li,{children:"Connection state tracking (Connecting \u2192 Connected \u2192 Disconnecting \u2192 Disconnected)"}),"\n",(0,i.jsx)(t.li,{children:"Document publishing pipelines (Draft \u2192 Review \u2192 Published / Archived)"}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"core-abstractions",children:"Core abstractions"}),"\n",(0,i.jsx)(t.h3,{id:"istatemachineconfiguratortstate-ttrigger",children:"IStateMachineConfigurator"}),"\n",(0,i.jsxs)(t.p,{children:["The entry point for defining a state machine. Call ",(0,i.jsx)(t.code,{children:"ForState"})," once per state to configure its transitions, then call ",(0,i.jsx)(t.code,{children:"Build"})," to create a running machine instance:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:"public interface IStateMachineConfigurator\r\n where TState : struct, Enum\r\n where TTrigger : struct, Enum\r\n{\r\n IStateConfigurator ForState(TState state);\r\n IStateMachine Build(TState initialState);\r\n}\n"})}),"\n",(0,i.jsx)(t.p,{children:"A single configurator instance can be used to build multiple independent machine instances with different initial states, all sharing the same transition configuration."}),"\n",(0,i.jsx)(t.h3,{id:"istateconfiguratortstate-ttrigger",children:"IStateConfigurator"}),"\n",(0,i.jsxs)(t.p,{children:["Returned by ",(0,i.jsx)(t.code,{children:"ForState"}),", used to define transitions and actions for a specific state:"]}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:"public interface IStateConfigurator\r\n where TState : struct, Enum\r\n where TTrigger : struct, Enum\r\n{\r\n IStateConfigurator Permit(TTrigger trigger, TState destinationState);\r\n IStateConfigurator PermitIf(TTrigger trigger, TState destinationState, Func guard);\r\n IStateConfigurator OnEntry(Func action);\r\n IStateConfigurator OnExit(Func action);\r\n}\n"})}),"\n",(0,i.jsx)(t.h3,{id:"istatemachinetstate-ttrigger",children:"IStateMachine"}),"\n",(0,i.jsx)(t.p,{children:"The running state machine instance. Inspect current state, check permitted triggers, and fire them:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-csharp",children:"public interface IStateMachine\r\n where TState : struct, Enum\r\n where TTrigger : struct, Enum\r\n{\r\n TState CurrentState { get; }\r\n bool CanFire(TTrigger trigger);\r\n IEnumerable PermittedTriggers { get; }\r\n Task FireAsync(TTrigger trigger, CancellationToken cancellationToken = default);\r\n Task FireAsync(TTrigger trigger, TData data, CancellationToken cancellationToken = default);\r\n}\n"})}),"\n",(0,i.jsx)(t.h2,{id:"stateless-vs-masstransit-adapter",children:"Stateless vs MassTransit adapter"}),"\n",(0,i.jsxs)(t.p,{children:["RCommon ships two adapters for the state machine abstraction. Both register as ",(0,i.jsx)(t.code,{children:"IStateMachineConfigurator"})," and produce ",(0,i.jsx)(t.code,{children:"IStateMachine"})," instances:"]}),"\n",(0,i.jsxs)(t.table,{children:[(0,i.jsx)(t.thead,{children:(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.th,{children:"Aspect"}),(0,i.jsx)(t.th,{children:(0,i.jsx)(t.code,{children:"RCommon.Stateless"})}),(0,i.jsx)(t.th,{children:(0,i.jsx)(t.code,{children:"RCommon.MassTransit.StateMachines"})})]})}),(0,i.jsxs)(t.tbody,{children:[(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:"Backing library"}),(0,i.jsxs)(t.td,{children:[(0,i.jsx)(t.code,{children:"Stateless"})," NuGet package"]}),(0,i.jsx)(t.td,{children:"Custom dictionary-based FSM (no extra dependency)"})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:"Parameterized triggers"}),(0,i.jsxs)(t.td,{children:["Fully supported via Stateless ",(0,i.jsx)(t.code,{children:"SetTriggerParameters"})]}),(0,i.jsx)(t.td,{children:"Accepted but data parameter is ignored"})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:"Configuration model"}),(0,i.jsxs)(t.td,{children:["Deferred: actions recorded and replayed on each ",(0,i.jsx)(t.code,{children:"Build"})," call"]}),(0,i.jsxs)(t.td,{children:["Shared dictionary cloned per ",(0,i.jsx)(t.code,{children:"Build"})," call"]})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:"Registration"}),(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"WithStatelessStateMachine()"})}),(0,i.jsx)(t.td,{children:(0,i.jsx)(t.code,{children:"WithMassTransitStateMachine()"})})]}),(0,i.jsxs)(t.tr,{children:[(0,i.jsx)(t.td,{children:"Suitable for"}),(0,i.jsx)(t.td,{children:"Most applications; rich guard and parameterized trigger support"}),(0,i.jsx)(t.td,{children:"Simple state tracking in messaging-heavy contexts"})]})]})]}),"\n",(0,i.jsxs)(t.p,{children:["Use ",(0,i.jsx)(t.code,{children:"RCommon.Stateless"})," unless you have a specific reason to prefer the dictionary-based adapter."]}),"\n",(0,i.jsx)(t.h2,{id:"choosing-an-adapter",children:"Choosing an adapter"}),"\n",(0,i.jsxs)(t.p,{children:["Choose ",(0,i.jsx)(t.code,{children:"RCommon.Stateless"})," when:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsx)(t.li,{children:"Your state machine needs parameterized triggers with data passed to entry actions."}),"\n",(0,i.jsx)(t.li,{children:"You want the full feature set of the Stateless library (sub-states, trigger parameters, dot-graph export)."}),"\n",(0,i.jsx)(t.li,{children:"You prefer a well-established open-source library as the engine."}),"\n"]}),"\n",(0,i.jsxs)(t.p,{children:["Choose ",(0,i.jsx)(t.code,{children:"RCommon.MassTransit.StateMachines"})," when:"]}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:["You are already using ",(0,i.jsx)(t.code,{children:"RCommon.MassTransit"})," and want to avoid an additional dependency."]}),"\n",(0,i.jsx)(t.li,{children:"Your state machine only uses simple unconditional transitions and guarded transitions with no trigger data."}),"\n"]}),"\n",(0,i.jsx)(t.h2,{id:"section-contents",children:"Section contents"}),"\n",(0,i.jsxs)(t.ul,{children:["\n",(0,i.jsxs)(t.li,{children:[(0,i.jsx)(t.a,{href:"/docs/next/state-machines/stateless",children:"Stateless"})," \u2014 Stateless library integration, defining states/triggers, full configuration reference"]}),"\n"]})]})}function h(e={}){const{wrapper:t}={...(0,a.R)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(l,{...e})}):l(e)}},81753(e,t,n){n.d(t,{R:()=>s,x:()=>c});var i=n(30758);const a={},r=i.createContext(a);function s(e){const t=i.useContext(r);return i.useMemo(function(){return"function"==typeof e?e(t):{...t,...e}},[t,e])}function c(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),i.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/1900a2f2.50e47a8a.js b/website/build/assets/js/1900a2f2.50e47a8a.js deleted file mode 100644 index 24d8856c..00000000 --- a/website/build/assets/js/1900a2f2.50e47a8a.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[688],{36806(e,n,i){i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>r,default:()=>h,frontMatter:()=>s,metadata:()=>c,toc:()=>d});var t=i(86070),o=i(81753);const s={title:"Overview & Philosophy",sidebar_position:1,description:"Learn what RCommon provides \u2014 pluggable .NET abstractions for persistence, CQRS, domain events, messaging, and caching with a fluent DI-first configuration API."},r="Overview & Philosophy",c={id:"getting-started/overview",title:"Overview & Philosophy",description:"Learn what RCommon provides \u2014 pluggable .NET abstractions for persistence, CQRS, domain events, messaging, and caching with a fluent DI-first configuration API.",source:"@site/docs/getting-started/overview.mdx",sourceDirName:"getting-started",slug:"/getting-started/overview",permalink:"/docs/next/getting-started/overview",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/getting-started/overview.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Overview & Philosophy",sidebar_position:1,description:"Learn what RCommon provides \u2014 pluggable .NET abstractions for persistence, CQRS, domain events, messaging, and caching with a fluent DI-first configuration API."},sidebar:"docsSidebar",previous:{title:"Getting Started",permalink:"/docs/next/category/getting-started"},next:{title:"Installation",permalink:"/docs/next/getting-started/installation"}},a={},d=[{value:"What RCommon provides",id:"what-rcommon-provides",level:2},{value:"What RCommon is not",id:"what-rcommon-is-not",level:2},{value:"Supported frameworks",id:"supported-frameworks",level:2},{value:"License",id:"license",level:2},{value:"Source code",id:"source-code",level:2},{value:"Design principles",id:"design-principles",level:2}];function l(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",strong:"strong",ul:"ul",...(0,o.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"overview--philosophy",children:"Overview & Philosophy"}),"\n",(0,t.jsx)(n.p,{children:"RCommon is a .NET infrastructure library that provides battle-tested abstractions for the concerns common to every production application: persistence, CQRS/mediator, event handling, caching, validation, emailing, and more."}),"\n",(0,t.jsx)(n.p,{children:"The goal is not to invent new patterns but to give you clean, consistent interfaces over the implementations you already use \u2014 Entity Framework Core, MediatR, MassTransit, Wolverine, Redis, and others \u2014 so that swapping providers never touches your domain or application code."}),"\n",(0,t.jsx)(n.h2,{id:"what-rcommon-provides",children:"What RCommon provides"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Fluent, DI-first configuration"})," \u2014 a single ",(0,t.jsx)(n.code,{children:"AddRCommon()"})," call bootstraps everything through a composable builder chain."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Pluggable providers"})," \u2014 every feature area exposes a generic slot (",(0,t.jsx)(n.code,{children:"WithPersistence"}),", ",(0,t.jsx)(n.code,{children:"WithMediator"}),", ",(0,t.jsx)(n.code,{children:"WithEventHandling"}),", etc.) that accepts whichever implementation package you install."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Repository and unit-of-work abstractions"})," \u2014 ",(0,t.jsx)(n.code,{children:"ILinqRepository"}),", ",(0,t.jsx)(n.code,{children:"IReadOnlyRepository"}),", ",(0,t.jsx)(n.code,{children:"IWriteOnlyRepository"}),", and ",(0,t.jsx)(n.code,{children:"IAggregateRepository"})," sit in front of EF Core, Dapper, or Linq2Db."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Domain-driven design primitives"})," \u2014 base entity classes with auditing, soft delete, and transactional domain events built in."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"In-process event bus"})," \u2014 ",(0,t.jsx)(n.code,{children:"IEventBus"})," with subscriber registration and a transactional ",(0,t.jsx)(n.code,{children:"IEventRouter"})," that coordinates domain events with the unit of work."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"CQRS mediation"})," \u2014 a thin ",(0,t.jsx)(n.code,{children:"IMediator"})," abstraction backed by MediatR or Wolverine."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Utility types"})," \u2014 ",(0,t.jsx)(n.code,{children:"Guard"}),", ",(0,t.jsx)(n.code,{children:"ISystemTime"}),", ",(0,t.jsx)(n.code,{children:"IGuidGenerator"}),", ",(0,t.jsx)(n.code,{children:"ICommonFactory"}),", specification pattern support, and a rich set of extension methods."]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"what-rcommon-is-not",children:"What RCommon is not"}),"\n",(0,t.jsx)(n.p,{children:"RCommon does not dictate your architecture. It does not generate controllers, scaffold screens, impose a folder structure, or require you to inherit from framework base classes in your domain layer. You choose Clean Architecture, Vertical Slice, Modular Monolith, or anything else \u2014 RCommon works alongside whatever structure you adopt."}),"\n",(0,t.jsx)(n.h2,{id:"supported-frameworks",children:"Supported frameworks"}),"\n",(0,t.jsx)(n.p,{children:"RCommon targets:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:".NET 8 (LTS)"}),"\n",(0,t.jsx)(n.li,{children:".NET 9"}),"\n",(0,t.jsx)(n.li,{children:".NET 10"}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"license",children:"License"}),"\n",(0,t.jsxs)(n.p,{children:["RCommon is released under the ",(0,t.jsx)(n.a,{href:"https://www.apache.org/licenses/LICENSE-2.0",children:"Apache License, Version 2.0"}),". It is free for commercial and open-source use."]}),"\n",(0,t.jsx)(n.h2,{id:"source-code",children:"Source code"}),"\n",(0,t.jsxs)(n.p,{children:["The full source is available on GitHub: ",(0,t.jsx)(n.a,{href:"https://github.com/RCommon-Framework/RCommon",children:"https://github.com/jfrog/RCommon"})]}),"\n",(0,t.jsx)(n.h2,{id:"design-principles",children:"Design principles"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Abstractions over implementations."})," Your application code depends on ",(0,t.jsx)(n.code,{children:"ILinqRepository"}),", not ",(0,t.jsx)(n.code,{children:"EFCoreRepository"}),". The concrete type is registered at the composition root and can be swapped without touching business logic."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Fluent composition over convention magic."})," Every feature is opt-in. If you do not call ",(0,t.jsx)(n.code,{children:"WithPersistence()"}),", no persistence services are registered. There is no scanning, no implicit wiring, and no surprises."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Single entry point."})," ",(0,t.jsx)(n.code,{children:"services.AddRCommon()"})," returns an ",(0,t.jsx)(n.code,{children:"IRCommonBuilder"}),". From there, every subsystem is a method call on that builder. The chain is self-documenting and IDE-discoverable."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.strong,{children:"Testability."})," Abstractions like ",(0,t.jsx)(n.code,{children:"ISystemTime"})," and ",(0,t.jsx)(n.code,{children:"IGuidGenerator"})," exist specifically so that deterministic unit tests are straightforward without mocking static members or ",(0,t.jsx)(n.code,{children:"DateTime.Now"}),"."]})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(l,{...e})}):l(e)}},81753(e,n,i){i.d(n,{R:()=>r,x:()=>c});var t=i(30758);const o={},s=t.createContext(o);function r(e){const n=t.useContext(s);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),t.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/1900a2f2.6fdfc7ab.js b/website/build/assets/js/1900a2f2.6fdfc7ab.js new file mode 100644 index 00000000..5fa40ae9 --- /dev/null +++ b/website/build/assets/js/1900a2f2.6fdfc7ab.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[688],{36806(e,n,i){i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>r,default:()=>h,frontMatter:()=>s,metadata:()=>c,toc:()=>d});var o=i(86070),t=i(81753);const s={title:"Overview & Philosophy",sidebar_position:1,description:"Learn what RCommon provides \u2014 pluggable .NET abstractions for persistence, CQRS, domain events, messaging, and caching with a fluent DI-first configuration API."},r="Overview & Philosophy",c={id:"getting-started/overview",title:"Overview & Philosophy",description:"Learn what RCommon provides \u2014 pluggable .NET abstractions for persistence, CQRS, domain events, messaging, and caching with a fluent DI-first configuration API.",source:"@site/docs/getting-started/overview.mdx",sourceDirName:"getting-started",slug:"/getting-started/overview",permalink:"/docs/next/getting-started/overview",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/getting-started/overview.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Overview & Philosophy",sidebar_position:1,description:"Learn what RCommon provides \u2014 pluggable .NET abstractions for persistence, CQRS, domain events, messaging, and caching with a fluent DI-first configuration API."},sidebar:"docsSidebar",previous:{title:"Getting Started",permalink:"/docs/next/category/getting-started"},next:{title:"Installation",permalink:"/docs/next/getting-started/installation"}},a={},d=[{value:"What RCommon provides",id:"what-rcommon-provides",level:2},{value:"What RCommon is not",id:"what-rcommon-is-not",level:2},{value:"Supported frameworks",id:"supported-frameworks",level:2},{value:"License",id:"license",level:2},{value:"Source code",id:"source-code",level:2},{value:"Design principles",id:"design-principles",level:2}];function l(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",strong:"strong",ul:"ul",...(0,t.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.h1,{id:"overview--philosophy",children:"Overview & Philosophy"}),"\n",(0,o.jsx)(n.p,{children:"RCommon is a .NET infrastructure library that provides battle-tested abstractions for the concerns common to every production application: persistence, CQRS/mediator, event handling, caching, validation, emailing, and more."}),"\n",(0,o.jsx)(n.p,{children:"The goal is not to invent new patterns but to give you clean, consistent interfaces over the implementations you already use \u2014 Entity Framework Core, MediatR, MassTransit, Wolverine, Redis, and others \u2014 so that swapping providers never touches your domain or application code."}),"\n",(0,o.jsx)(n.h2,{id:"what-rcommon-provides",children:"What RCommon provides"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"Fluent, DI-first configuration"})," \u2014 a single ",(0,o.jsx)(n.code,{children:"AddRCommon()"})," call bootstraps everything through a composable builder chain. RCommon supports both single-call and multi-module composition; see ",(0,o.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"}),"."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"Pluggable providers"})," \u2014 every feature area exposes a generic slot (",(0,o.jsx)(n.code,{children:"WithPersistence"}),", ",(0,o.jsx)(n.code,{children:"WithMediator"}),", ",(0,o.jsx)(n.code,{children:"WithEventHandling"}),", etc.) that accepts whichever implementation package you install."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"Repository and unit-of-work abstractions"})," \u2014 ",(0,o.jsx)(n.code,{children:"ILinqRepository"}),", ",(0,o.jsx)(n.code,{children:"IReadOnlyRepository"}),", ",(0,o.jsx)(n.code,{children:"IWriteOnlyRepository"}),", and ",(0,o.jsx)(n.code,{children:"IAggregateRepository"})," sit in front of EF Core, Dapper, or Linq2Db."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"Domain-driven design primitives"})," \u2014 base entity classes with auditing, soft delete, and transactional domain events built in."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"In-process event bus"})," \u2014 ",(0,o.jsx)(n.code,{children:"IEventBus"})," with subscriber registration and a transactional ",(0,o.jsx)(n.code,{children:"IEventRouter"})," that coordinates domain events with the unit of work."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"CQRS mediation"})," \u2014 a thin ",(0,o.jsx)(n.code,{children:"IMediator"})," abstraction backed by MediatR or Wolverine."]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.strong,{children:"Utility types"})," \u2014 ",(0,o.jsx)(n.code,{children:"Guard"}),", ",(0,o.jsx)(n.code,{children:"ISystemTime"}),", ",(0,o.jsx)(n.code,{children:"IGuidGenerator"}),", ",(0,o.jsx)(n.code,{children:"ICommonFactory"}),", specification pattern support, and a rich set of extension methods."]}),"\n"]}),"\n",(0,o.jsx)(n.h2,{id:"what-rcommon-is-not",children:"What RCommon is not"}),"\n",(0,o.jsx)(n.p,{children:"RCommon does not dictate your architecture. It does not generate controllers, scaffold screens, impose a folder structure, or require you to inherit from framework base classes in your domain layer. You choose Clean Architecture, Vertical Slice, Modular Monolith, or anything else \u2014 RCommon works alongside whatever structure you adopt."}),"\n",(0,o.jsx)(n.h2,{id:"supported-frameworks",children:"Supported frameworks"}),"\n",(0,o.jsx)(n.p,{children:"RCommon targets:"}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsx)(n.li,{children:".NET 8 (LTS)"}),"\n",(0,o.jsx)(n.li,{children:".NET 9"}),"\n",(0,o.jsx)(n.li,{children:".NET 10"}),"\n"]}),"\n",(0,o.jsx)(n.h2,{id:"license",children:"License"}),"\n",(0,o.jsxs)(n.p,{children:["RCommon is released under the ",(0,o.jsx)(n.a,{href:"https://www.apache.org/licenses/LICENSE-2.0",children:"Apache License, Version 2.0"}),". It is free for commercial and open-source use."]}),"\n",(0,o.jsx)(n.h2,{id:"source-code",children:"Source code"}),"\n",(0,o.jsxs)(n.p,{children:["The full source is available on GitHub: ",(0,o.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon",children:"https://github.com/RCommon-Team/RCommon"})]}),"\n",(0,o.jsx)(n.h2,{id:"design-principles",children:"Design principles"}),"\n",(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.strong,{children:"Abstractions over implementations."})," Your application code depends on ",(0,o.jsx)(n.code,{children:"ILinqRepository"}),", not ",(0,o.jsx)(n.code,{children:"EFCoreRepository"}),". The concrete type is registered at the composition root and can be swapped without touching business logic."]}),"\n",(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.strong,{children:"Fluent composition over convention magic."})," Every feature is opt-in. If you do not call ",(0,o.jsx)(n.code,{children:"WithPersistence()"}),", no persistence services are registered. There is no scanning, no implicit wiring, and no surprises."]}),"\n",(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.strong,{children:"Single entry point."})," ",(0,o.jsx)(n.code,{children:"services.AddRCommon()"})," returns an ",(0,o.jsx)(n.code,{children:"IRCommonBuilder"}),". From there, every subsystem is a method call on that builder. The chain is self-documenting and IDE-discoverable."]}),"\n",(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.strong,{children:"Testability."})," Abstractions like ",(0,o.jsx)(n.code,{children:"ISystemTime"})," and ",(0,o.jsx)(n.code,{children:"IGuidGenerator"})," exist specifically so that deterministic unit tests are straightforward without mocking static members or ",(0,o.jsx)(n.code,{children:"DateTime.Now"}),"."]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(l,{...e})}):l(e)}},81753(e,n,i){i.d(n,{R:()=>r,x:()=>c});var o=i(30758);const t={},s=o.createContext(t);function r(e){const n=o.useContext(s);return o.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:r(e.components),o.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/195c7c19.17c6dcea.js b/website/build/assets/js/195c7c19.17c6dcea.js deleted file mode 100644 index 84f6abc1..00000000 --- a/website/build/assets/js/195c7c19.17c6dcea.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[2617],{76542(e,i,n){n.r(i),n.d(i,{assets:()=>s,contentTitle:()=>r,default:()=>m,frontMatter:()=>d,metadata:()=>o,toc:()=>c});var a=n(86070),t=n(81753),l=n(75783);const d={title:"FluentValidation",sidebar_position:1,description:"RCommon.FluentValidation integrates FluentValidation with the CQRS pipeline for automatic command and query validation, with manual IValidationService support."},r="FluentValidation",o={id:"validation/fluent-validation",title:"FluentValidation",description:"RCommon.FluentValidation integrates FluentValidation with the CQRS pipeline for automatic command and query validation, with manual IValidationService support.",source:"@site/docs/validation/fluent-validation.mdx",sourceDirName:"validation",slug:"/validation/fluent-validation",permalink:"/docs/next/validation/fluent-validation",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/validation/fluent-validation.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"FluentValidation",sidebar_position:1,description:"RCommon.FluentValidation integrates FluentValidation with the CQRS pipeline for automatic command and query validation, with manual IValidationService support."},sidebar:"docsSidebar",previous:{title:"Validation",permalink:"/docs/next/category/validation"},next:{title:"Email",permalink:"/docs/next/category/email"}},s={},c=[{value:"Overview",id:"overview",level:2},{value:"How validation works",id:"how-validation-works",level:3},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Scan an assembly for all validators",id:"scan-an-assembly-for-all-validators",level:3},{value:"Scan multiple assemblies",id:"scan-multiple-assemblies",level:3},{value:"Register a single validator explicitly",id:"register-a-single-validator-explicitly",level:3},{value:"Enable automatic CQRS pipeline validation",id:"enable-automatic-cqrs-pipeline-validation",level:3},{value:"Usage",id:"usage",level:2},{value:"Defining a validator",id:"defining-a-validator",level:3},{value:"Validating manually via IValidationService",id:"validating-manually-via-ivalidationservice",level:3},{value:"Letting the CQRS pipeline validate automatically",id:"letting-the-cqrs-pipeline-validate-automatically",level:3},{value:"Handling ValidationException",id:"handling-validationexception",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"IRCommonBuilder extension",id:"ircommonbuilder-extension",level:3},{value:"IFluentValidationBuilder extension methods",id:"ifluentvalidationbuilder-extension-methods",level:3},{value:"CqrsValidationOptions",id:"cqrsvalidationoptions",level:3},{value:"IValidationService",id:"ivalidationservice",level:3},{value:"ValidationOutcome",id:"validationoutcome",level:3},{value:"ValidationFault",id:"validationfault",level:3}];function h(e){const i={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(i.h1,{id:"fluentvalidation",children:"FluentValidation"}),"\n",(0,a.jsx)(i.h2,{id:"overview",children:"Overview"}),"\n",(0,a.jsxs)(i.p,{children:[(0,a.jsx)(i.code,{children:"RCommon.FluentValidation"})," integrates the FluentValidation library with RCommon's validation pipeline. It registers ",(0,a.jsx)(i.code,{children:"FluentValidationProvider"})," as the ",(0,a.jsx)(i.code,{children:"IValidationProvider"})," implementation and wires it into the CQRS command and query buses, so validation runs automatically before handlers execute."]}),"\n",(0,a.jsxs)(i.p,{children:["Your application code depends only on ",(0,a.jsx)(i.code,{children:"IValidationService"})," (for manual validation) or on the CQRS pipeline behavior (for automatic validation). The FluentValidation-specific types stay confined to startup configuration and the validator classes themselves."]}),"\n",(0,a.jsx)(i.h3,{id:"how-validation-works",children:"How validation works"}),"\n",(0,a.jsxs)(i.ol,{children:["\n",(0,a.jsxs)(i.li,{children:["You define validators by extending ",(0,a.jsx)(i.code,{children:"AbstractValidator"})," and declaring rules with FluentValidation's fluent API."]}),"\n",(0,a.jsx)(i.li,{children:"Validators are registered in the DI container during startup."}),"\n",(0,a.jsxs)(i.li,{children:["When a command or query is dispatched (with CQRS validation enabled), ",(0,a.jsx)(i.code,{children:"FluentValidationProvider"})," resolves all ",(0,a.jsx)(i.code,{children:"IValidator"})," instances for that type, runs them in parallel, and collects the results into a ",(0,a.jsx)(i.code,{children:"ValidationOutcome"}),"."]}),"\n",(0,a.jsxs)(i.li,{children:["If there are failures and ",(0,a.jsx)(i.code,{children:"throwOnFaults"})," is ",(0,a.jsx)(i.code,{children:"true"}),", a ",(0,a.jsx)(i.code,{children:"ValidationException"})," is thrown containing a list of ",(0,a.jsx)(i.code,{children:"ValidationFault"})," objects \u2014 each carrying the property name, error message, and attempted value."]}),"\n"]}),"\n",(0,a.jsxs)(i.p,{children:["You can also call ",(0,a.jsx)(i.code,{children:"IValidationService.ValidateAsync"})," directly from application services when you need to validate outside the CQRS pipeline."]}),"\n",(0,a.jsx)(i.h2,{id:"installation",children:"Installation"}),"\n",(0,a.jsx)(l.A,{packageName:"RCommon.FluentValidation"}),"\n",(0,a.jsx)(i.h2,{id:"configuration",children:"Configuration"}),"\n",(0,a.jsxs)(i.p,{children:["Call ",(0,a.jsx)(i.code,{children:"WithValidation"})," inside your ",(0,a.jsx)(i.code,{children:"AddRCommon()"})," block. Use the configuration action to register your validators."]}),"\n",(0,a.jsx)(i.h3,{id:"scan-an-assembly-for-all-validators",children:"Scan an assembly for all validators"}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:"using RCommon;\r\nusing RCommon.ApplicationServices;\r\nusing RCommon.FluentValidation;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithValidation(validation =>\r\n {\r\n validation.AddValidatorsFromAssemblyContaining(typeof(MyCommand));\r\n });\n"})}),"\n",(0,a.jsxs)(i.p,{children:[(0,a.jsx)(i.code,{children:"AddValidatorsFromAssemblyContaining"})," scans the assembly that contains the given type and registers every ",(0,a.jsx)(i.code,{children:"AbstractValidator"})," implementation it finds."]}),"\n",(0,a.jsx)(i.h3,{id:"scan-multiple-assemblies",children:"Scan multiple assemblies"}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithValidation(validation =>\r\n {\r\n validation.AddValidatorsFromAssemblies(new[]\r\n {\r\n typeof(CreateOrderCommand).Assembly,\r\n typeof(ShipOrderCommand).Assembly\r\n });\r\n });\n"})}),"\n",(0,a.jsx)(i.h3,{id:"register-a-single-validator-explicitly",children:"Register a single validator explicitly"}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithValidation(validation =>\r\n {\r\n validation.AddValidator();\r\n });\n"})}),"\n",(0,a.jsx)(i.h3,{id:"enable-automatic-cqrs-pipeline-validation",children:"Enable automatic CQRS pipeline validation"}),"\n",(0,a.jsxs)(i.p,{children:["Pass ",(0,a.jsx)(i.code,{children:"CqrsValidationOptions"})," to activate validation in the command bus, the query bus, or both:"]}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithValidation(validation =>\r\n {\r\n validation.AddValidatorsFromAssemblyContaining(typeof(MyCommand));\r\n })\r\n // Enable auto-validation for commands only.\r\n .WithValidation(opts =>\r\n {\r\n opts.ValidateCommands = true;\r\n opts.ValidateQueries = false;\r\n });\n"})}),"\n",(0,a.jsxs)(i.p,{children:["Alternatively, pass ",(0,a.jsx)(i.code,{children:"CqrsValidationOptions"})," in the single ",(0,a.jsx)(i.code,{children:"WithValidation"})," overload that accepts it:"]}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithValidation(opts =>\r\n {\r\n opts.ValidateCommands = true;\r\n opts.ValidateQueries = true;\r\n });\n"})}),"\n",(0,a.jsxs)(i.p,{children:["Both ",(0,a.jsx)(i.code,{children:"ValidateCommands"})," and ",(0,a.jsx)(i.code,{children:"ValidateQueries"})," default to ",(0,a.jsx)(i.code,{children:"false"}),"."]}),"\n",(0,a.jsx)(i.h2,{id:"usage",children:"Usage"}),"\n",(0,a.jsx)(i.h3,{id:"defining-a-validator",children:"Defining a validator"}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:'using FluentValidation;\r\n\r\npublic class CreateOrderCommand\r\n{\r\n public string CustomerId { get; set; } = string.Empty;\r\n public IReadOnlyList Lines { get; set; } = [];\r\n}\r\n\r\npublic class CreateOrderCommandValidator : AbstractValidator\r\n{\r\n public CreateOrderCommandValidator()\r\n {\r\n RuleFor(cmd => cmd.CustomerId)\r\n .NotEmpty()\r\n .WithMessage("Customer ID is required.");\r\n\r\n RuleFor(cmd => cmd.Lines)\r\n .NotEmpty()\r\n .WithMessage("An order must have at least one line.");\r\n }\r\n}\n'})}),"\n",(0,a.jsx)(i.h3,{id:"validating-manually-via-ivalidationservice",children:"Validating manually via IValidationService"}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:"public class OrderApplicationService\r\n{\r\n private readonly IValidationService _validationService;\r\n private readonly IOrderRepository _repository;\r\n\r\n public OrderApplicationService(\r\n IValidationService validationService,\r\n IOrderRepository repository)\r\n {\r\n _validationService = validationService;\r\n _repository = repository;\r\n }\r\n\r\n public async Task CreateOrderAsync(\r\n CreateOrderCommand command,\r\n CancellationToken ct = default)\r\n {\r\n ValidationOutcome outcome =\r\n await _validationService.ValidateAsync(command, throwOnFaults: false, ct);\r\n\r\n if (outcome.Errors.Any())\r\n {\r\n return outcome;\r\n }\r\n\r\n await _repository.AddAsync(Order.From(command), ct);\r\n return outcome;\r\n }\r\n}\n"})}),"\n",(0,a.jsx)(i.h3,{id:"letting-the-cqrs-pipeline-validate-automatically",children:"Letting the CQRS pipeline validate automatically"}),"\n",(0,a.jsxs)(i.p,{children:["When ",(0,a.jsx)(i.code,{children:"ValidateCommands"})," is ",(0,a.jsx)(i.code,{children:"true"}),", the command bus validates the command before dispatching to the handler. If validation fails, a ",(0,a.jsx)(i.code,{children:"ValidationException"})," is thrown before your handler is invoked \u2014 no explicit validation code is needed in the handler."]}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:"public class CreateOrderCommandHandler : ICommandHandler\r\n{\r\n private readonly IOrderRepository _repository;\r\n\r\n public CreateOrderCommandHandler(IOrderRepository repository)\r\n {\r\n _repository = repository;\r\n }\r\n\r\n // The pipeline has already validated the command by the time this runs.\r\n public async Task HandleAsync(CreateOrderCommand command, CancellationToken ct)\r\n {\r\n await _repository.AddAsync(Order.From(command), ct);\r\n }\r\n}\n"})}),"\n",(0,a.jsx)(i.h3,{id:"handling-validationexception",children:"Handling ValidationException"}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:'try\r\n{\r\n await commandBus.SendAsync(new CreateOrderCommand());\r\n}\r\ncatch (ValidationException ex)\r\n{\r\n foreach (ValidationFault fault in ex.Faults)\r\n {\r\n Console.WriteLine($"{fault.PropertyName}: {fault.Message}");\r\n }\r\n}\n'})}),"\n",(0,a.jsx)(i.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,a.jsxs)(i.h3,{id:"ircommonbuilder-extension",children:[(0,a.jsx)(i.code,{children:"IRCommonBuilder"})," extension"]}),"\n",(0,a.jsxs)(i.table,{children:[(0,a.jsx)(i.thead,{children:(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.th,{children:"Method"}),(0,a.jsx)(i.th,{children:"Description"})]})}),(0,a.jsxs)(i.tbody,{children:[(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"WithValidation()"})}),(0,a.jsxs)(i.td,{children:["Registers the validation provider with default ",(0,a.jsx)(i.code,{children:"CqrsValidationOptions"}),"."]})]}),(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"WithValidation(Action)"})}),(0,a.jsx)(i.td,{children:"Registers the validation provider and configures CQRS pipeline validation."})]})]})]}),"\n",(0,a.jsxs)(i.h3,{id:"ifluentvalidationbuilder-extension-methods",children:[(0,a.jsx)(i.code,{children:"IFluentValidationBuilder"})," extension methods"]}),"\n",(0,a.jsxs)(i.table,{children:[(0,a.jsx)(i.thead,{children:(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.th,{children:"Method"}),(0,a.jsx)(i.th,{children:"Description"})]})}),(0,a.jsxs)(i.tbody,{children:[(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"AddValidator()"})}),(0,a.jsxs)(i.td,{children:["Registers a single validator for type ",(0,a.jsx)(i.code,{children:"T"})," with a scoped lifetime."]})]}),(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"AddValidatorsFromAssembly(assembly, lifetime?, filter?, includeInternal?)"})}),(0,a.jsx)(i.td,{children:"Scans the given assembly and registers all validators found."})]}),(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"AddValidatorsFromAssemblies(assemblies, lifetime?, filter?, includeInternal?)"})}),(0,a.jsx)(i.td,{children:"Scans multiple assemblies and registers all validators found."})]}),(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"AddValidatorsFromAssemblyContaining(type, lifetime?, filter?, includeInternal?)"})}),(0,a.jsx)(i.td,{children:"Scans the assembly containing the given type."})]})]})]}),"\n",(0,a.jsx)(i.h3,{id:"cqrsvalidationoptions",children:(0,a.jsx)(i.code,{children:"CqrsValidationOptions"})}),"\n",(0,a.jsxs)(i.table,{children:[(0,a.jsx)(i.thead,{children:(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.th,{children:"Property"}),(0,a.jsx)(i.th,{children:"Default"}),(0,a.jsx)(i.th,{children:"Description"})]})}),(0,a.jsxs)(i.tbody,{children:[(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"ValidateCommands"})}),(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"false"})}),(0,a.jsx)(i.td,{children:"Automatically validate commands before dispatch."})]}),(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"ValidateQueries"})}),(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"false"})}),(0,a.jsx)(i.td,{children:"Automatically validate queries before dispatch."})]})]})]}),"\n",(0,a.jsx)(i.h3,{id:"ivalidationservice",children:(0,a.jsx)(i.code,{children:"IValidationService"})}),"\n",(0,a.jsxs)(i.table,{children:[(0,a.jsx)(i.thead,{children:(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.th,{children:"Method"}),(0,a.jsx)(i.th,{children:"Description"})]})}),(0,a.jsx)(i.tbody,{children:(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"ValidateAsync(target, throwOnFaults?, cancellationToken?)"})}),(0,a.jsxs)(i.td,{children:["Validates ",(0,a.jsx)(i.code,{children:"target"})," and returns a ",(0,a.jsx)(i.code,{children:"ValidationOutcome"}),". Throws ",(0,a.jsx)(i.code,{children:"ValidationException"})," when ",(0,a.jsx)(i.code,{children:"throwOnFaults"})," is ",(0,a.jsx)(i.code,{children:"true"})," and there are failures."]})]})})]}),"\n",(0,a.jsx)(i.h3,{id:"validationoutcome",children:(0,a.jsx)(i.code,{children:"ValidationOutcome"})}),"\n",(0,a.jsxs)(i.p,{children:["Returned by ",(0,a.jsx)(i.code,{children:"IValidationService.ValidateAsync"}),". Contains ",(0,a.jsx)(i.code,{children:"IReadOnlyList Errors"}),"."]}),"\n",(0,a.jsx)(i.h3,{id:"validationfault",children:(0,a.jsx)(i.code,{children:"ValidationFault"})}),"\n",(0,a.jsxs)(i.table,{children:[(0,a.jsx)(i.thead,{children:(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.th,{children:"Property"}),(0,a.jsx)(i.th,{children:"Description"})]})}),(0,a.jsxs)(i.tbody,{children:[(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"PropertyName"})}),(0,a.jsx)(i.td,{children:"The name of the property that failed validation."})]}),(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"Message"})}),(0,a.jsx)(i.td,{children:"The human-readable error message."})]}),(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"AttemptedValue"})}),(0,a.jsx)(i.td,{children:"The value that was rejected."})]})]})]})]})}function m(e={}){const{wrapper:i}={...(0,t.R)(),...e.components};return i?(0,a.jsx)(i,{...e,children:(0,a.jsx)(h,{...e})}):h(e)}},75783(e,i,n){n.d(i,{A:()=>c});var a=n(30758);const t="container_xjrG",l="label_Y4p8",d="commandRow_FY5I",r="command_m7Qs",o="copyButton_u1GK";var s=n(86070);function c({packageName:e,version:i}){const[n,c]=(0,a.useState)(!1),h=i?`dotnet add package ${e} --version ${i}`:`dotnet add package ${e}`;return(0,s.jsxs)("div",{className:t,children:[(0,s.jsx)("div",{className:l,children:"NuGet Package"}),(0,s.jsxs)("div",{className:d,children:[(0,s.jsx)("code",{className:r,children:h}),(0,s.jsx)("button",{className:o,onClick:()=>{navigator.clipboard.writeText(h),c(!0),setTimeout(()=>c(!1),2e3)},title:"Copy to clipboard",children:n?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,i,n){n.d(i,{R:()=>d,x:()=>r});var a=n(30758);const t={},l=a.createContext(t);function d(e){const i=a.useContext(l);return a.useMemo(function(){return"function"==typeof e?e(i):{...i,...e}},[i,e])}function r(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:d(e.components),a.createElement(l.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/195c7c19.183c646e.js b/website/build/assets/js/195c7c19.183c646e.js new file mode 100644 index 00000000..4ffac363 --- /dev/null +++ b/website/build/assets/js/195c7c19.183c646e.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[2617],{76542(e,i,n){n.r(i),n.d(i,{assets:()=>s,contentTitle:()=>o,default:()=>m,frontMatter:()=>l,metadata:()=>r,toc:()=>c});var a=n(86070),t=n(81753),d=n(75783);const l={title:"FluentValidation",sidebar_position:1,description:"RCommon.FluentValidation integrates FluentValidation with the CQRS pipeline for automatic command and query validation, with manual IValidationService support."},o="FluentValidation",r={id:"validation/fluent-validation",title:"FluentValidation",description:"RCommon.FluentValidation integrates FluentValidation with the CQRS pipeline for automatic command and query validation, with manual IValidationService support.",source:"@site/docs/validation/fluent-validation.mdx",sourceDirName:"validation",slug:"/validation/fluent-validation",permalink:"/docs/next/validation/fluent-validation",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/validation/fluent-validation.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"FluentValidation",sidebar_position:1,description:"RCommon.FluentValidation integrates FluentValidation with the CQRS pipeline for automatic command and query validation, with manual IValidationService support."},sidebar:"docsSidebar",previous:{title:"Validation",permalink:"/docs/next/category/validation"},next:{title:"Email",permalink:"/docs/next/category/email"}},s={},c=[{value:"Overview",id:"overview",level:2},{value:"How validation works",id:"how-validation-works",level:3},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Scan an assembly for all validators",id:"scan-an-assembly-for-all-validators",level:3},{value:"Scan multiple assemblies",id:"scan-multiple-assemblies",level:3},{value:"Register a single validator explicitly",id:"register-a-single-validator-explicitly",level:3},{value:"Enable automatic CQRS pipeline validation",id:"enable-automatic-cqrs-pipeline-validation",level:3},{value:"Usage",id:"usage",level:2},{value:"Defining a validator",id:"defining-a-validator",level:3},{value:"Validating manually via IValidationService",id:"validating-manually-via-ivalidationservice",level:3},{value:"Letting the CQRS pipeline validate automatically",id:"letting-the-cqrs-pipeline-validate-automatically",level:3},{value:"Handling ValidationException",id:"handling-validationexception",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"IRCommonBuilder extension",id:"ircommonbuilder-extension",level:3},{value:"IFluentValidationBuilder extension methods",id:"ifluentvalidationbuilder-extension-methods",level:3},{value:"CqrsValidationOptions",id:"cqrsvalidationoptions",level:3},{value:"IValidationService",id:"ivalidationservice",level:3},{value:"ValidationOutcome",id:"validationoutcome",level:3},{value:"ValidationFault",id:"validationfault",level:3}];function h(e){const i={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(i.h1,{id:"fluentvalidation",children:"FluentValidation"}),"\n",(0,a.jsx)(i.h2,{id:"overview",children:"Overview"}),"\n",(0,a.jsxs)(i.p,{children:[(0,a.jsx)(i.code,{children:"RCommon.FluentValidation"})," integrates the FluentValidation library with RCommon's validation pipeline. It registers ",(0,a.jsx)(i.code,{children:"FluentValidationProvider"})," as the ",(0,a.jsx)(i.code,{children:"IValidationProvider"})," implementation and wires it into the CQRS command and query buses, so validation runs automatically before handlers execute."]}),"\n",(0,a.jsxs)(i.p,{children:["Your application code depends only on ",(0,a.jsx)(i.code,{children:"IValidationService"})," (for manual validation) or on the CQRS pipeline behavior (for automatic validation). The FluentValidation-specific types stay confined to startup configuration and the validator classes themselves."]}),"\n",(0,a.jsx)(i.h3,{id:"how-validation-works",children:"How validation works"}),"\n",(0,a.jsxs)(i.ol,{children:["\n",(0,a.jsxs)(i.li,{children:["You define validators by extending ",(0,a.jsx)(i.code,{children:"AbstractValidator"})," and declaring rules with FluentValidation's fluent API."]}),"\n",(0,a.jsx)(i.li,{children:"Validators are registered in the DI container during startup."}),"\n",(0,a.jsxs)(i.li,{children:["When a command or query is dispatched (with CQRS validation enabled), ",(0,a.jsx)(i.code,{children:"FluentValidationProvider"})," resolves all ",(0,a.jsx)(i.code,{children:"IValidator"})," instances for that type, runs them in parallel, and collects the results into a ",(0,a.jsx)(i.code,{children:"ValidationOutcome"}),"."]}),"\n",(0,a.jsxs)(i.li,{children:["If there are failures and ",(0,a.jsx)(i.code,{children:"throwOnFaults"})," is ",(0,a.jsx)(i.code,{children:"true"}),", a ",(0,a.jsx)(i.code,{children:"ValidationException"})," is thrown containing a list of ",(0,a.jsx)(i.code,{children:"ValidationFault"})," objects \u2014 each carrying the property name, error message, and attempted value."]}),"\n"]}),"\n",(0,a.jsxs)(i.p,{children:["You can also call ",(0,a.jsx)(i.code,{children:"IValidationService.ValidateAsync"})," directly from application services when you need to validate outside the CQRS pipeline."]}),"\n",(0,a.jsx)(i.h2,{id:"installation",children:"Installation"}),"\n",(0,a.jsx)(d.A,{packageName:"RCommon.FluentValidation"}),"\n",(0,a.jsx)(i.h2,{id:"configuration",children:"Configuration"}),"\n",(0,a.jsxs)(i.p,{children:["Call ",(0,a.jsx)(i.code,{children:"WithValidation"})," inside your ",(0,a.jsx)(i.code,{children:"AddRCommon()"})," block. Use the configuration action to register your validators."]}),"\n",(0,a.jsx)(i.h3,{id:"scan-an-assembly-for-all-validators",children:"Scan an assembly for all validators"}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:"using RCommon;\nusing RCommon.ApplicationServices;\nusing RCommon.FluentValidation;\n\nbuilder.Services.AddRCommon()\n .WithValidation(validation =>\n {\n validation.AddValidatorsFromAssemblyContaining(typeof(MyCommand));\n });\n"})}),"\n",(0,a.jsxs)(i.p,{children:[(0,a.jsx)(i.code,{children:"AddValidatorsFromAssemblyContaining"})," scans the assembly that contains the given type and registers every ",(0,a.jsx)(i.code,{children:"AbstractValidator"})," implementation it finds."]}),"\n",(0,a.jsx)(i.h3,{id:"scan-multiple-assemblies",children:"Scan multiple assemblies"}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithValidation(validation =>\n {\n validation.AddValidatorsFromAssemblies(new[]\n {\n typeof(CreateOrderCommand).Assembly,\n typeof(ShipOrderCommand).Assembly\n });\n });\n"})}),"\n",(0,a.jsx)(i.h3,{id:"register-a-single-validator-explicitly",children:"Register a single validator explicitly"}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithValidation(validation =>\n {\n validation.AddValidator();\n });\n"})}),"\n",(0,a.jsx)(i.h3,{id:"enable-automatic-cqrs-pipeline-validation",children:"Enable automatic CQRS pipeline validation"}),"\n",(0,a.jsxs)(i.p,{children:["Pass ",(0,a.jsx)(i.code,{children:"CqrsValidationOptions"})," to activate validation in the command bus, the query bus, or both:"]}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithValidation(validation =>\n {\n validation.AddValidatorsFromAssemblyContaining(typeof(MyCommand));\n })\n // Enable auto-validation for commands only.\n .WithValidation(opts =>\n {\n opts.ValidateCommands = true;\n opts.ValidateQueries = false;\n });\n"})}),"\n",(0,a.jsxs)(i.p,{children:["Alternatively, pass ",(0,a.jsx)(i.code,{children:"CqrsValidationOptions"})," in the single ",(0,a.jsx)(i.code,{children:"WithValidation"})," overload that accepts it:"]}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithValidation(opts =>\n {\n opts.ValidateCommands = true;\n opts.ValidateQueries = true;\n });\n"})}),"\n",(0,a.jsxs)(i.p,{children:["Both ",(0,a.jsx)(i.code,{children:"ValidateCommands"})," and ",(0,a.jsx)(i.code,{children:"ValidateQueries"})," default to ",(0,a.jsx)(i.code,{children:"false"}),"."]}),"\n",(0,a.jsx)(i.admonition,{title:"Modular composition",type:"tip",children:(0,a.jsxs)(i.p,{children:[(0,a.jsx)(i.code,{children:"WithValidation"})," is cache-aware. Both the ",(0,a.jsx)(i.code,{children:"RCommon.ApplicationServices"})," overload (",(0,a.jsx)(i.code,{children:"WithValidation(...)"}),") and the ",(0,a.jsx)(i.code,{children:"RCommon.FluentValidation"})," extension share the same builder cache, so when multiple modules call ",(0,a.jsx)(i.code,{children:"WithValidation(...)"})," the cached ",(0,a.jsx)(i.code,{children:"FluentValidationBuilder"})," is reused and each module's ",(0,a.jsx)(i.code,{children:"AddValidator"}),", ",(0,a.jsx)(i.code,{children:"AddValidatorsFromAssemblyContaining(...)"}),", and ",(0,a.jsx)(i.code,{children:"AddValidatorsFromAssemblies(...)"})," registrations accumulate on the same instance. Each module can scan its own assembly for validators without coordinating with other modules. ",(0,a.jsx)(i.code,{children:"CqrsValidationOptions"})," is a shared singleton: the last module to call the options overload wins for ",(0,a.jsx)(i.code,{children:"ValidateCommands"})," / ",(0,a.jsx)(i.code,{children:"ValidateQueries"}),". See ",(0,a.jsx)(i.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,a.jsx)(i.h2,{id:"usage",children:"Usage"}),"\n",(0,a.jsx)(i.h3,{id:"defining-a-validator",children:"Defining a validator"}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:'using FluentValidation;\n\npublic class CreateOrderCommand\n{\n public string CustomerId { get; set; } = string.Empty;\n public IReadOnlyList Lines { get; set; } = [];\n}\n\npublic class CreateOrderCommandValidator : AbstractValidator\n{\n public CreateOrderCommandValidator()\n {\n RuleFor(cmd => cmd.CustomerId)\n .NotEmpty()\n .WithMessage("Customer ID is required.");\n\n RuleFor(cmd => cmd.Lines)\n .NotEmpty()\n .WithMessage("An order must have at least one line.");\n }\n}\n'})}),"\n",(0,a.jsx)(i.h3,{id:"validating-manually-via-ivalidationservice",children:"Validating manually via IValidationService"}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:"public class OrderApplicationService\n{\n private readonly IValidationService _validationService;\n private readonly IOrderRepository _repository;\n\n public OrderApplicationService(\n IValidationService validationService,\n IOrderRepository repository)\n {\n _validationService = validationService;\n _repository = repository;\n }\n\n public async Task CreateOrderAsync(\n CreateOrderCommand command,\n CancellationToken ct = default)\n {\n ValidationOutcome outcome =\n await _validationService.ValidateAsync(command, throwOnFaults: false, ct);\n\n if (outcome.Errors.Any())\n {\n return outcome;\n }\n\n await _repository.AddAsync(Order.From(command), ct);\n return outcome;\n }\n}\n"})}),"\n",(0,a.jsx)(i.h3,{id:"letting-the-cqrs-pipeline-validate-automatically",children:"Letting the CQRS pipeline validate automatically"}),"\n",(0,a.jsxs)(i.p,{children:["When ",(0,a.jsx)(i.code,{children:"ValidateCommands"})," is ",(0,a.jsx)(i.code,{children:"true"}),", the command bus validates the command before dispatching to the handler. If validation fails, a ",(0,a.jsx)(i.code,{children:"ValidationException"})," is thrown before your handler is invoked \u2014 no explicit validation code is needed in the handler."]}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:"public class CreateOrderCommandHandler : ICommandHandler\n{\n private readonly IOrderRepository _repository;\n\n public CreateOrderCommandHandler(IOrderRepository repository)\n {\n _repository = repository;\n }\n\n // The pipeline has already validated the command by the time this runs.\n public async Task HandleAsync(CreateOrderCommand command, CancellationToken ct)\n {\n await _repository.AddAsync(Order.From(command), ct);\n }\n}\n"})}),"\n",(0,a.jsx)(i.h3,{id:"handling-validationexception",children:"Handling ValidationException"}),"\n",(0,a.jsx)(i.pre,{children:(0,a.jsx)(i.code,{className:"language-csharp",children:'try\n{\n await commandBus.SendAsync(new CreateOrderCommand());\n}\ncatch (ValidationException ex)\n{\n foreach (ValidationFault fault in ex.Faults)\n {\n Console.WriteLine($"{fault.PropertyName}: {fault.Message}");\n }\n}\n'})}),"\n",(0,a.jsx)(i.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,a.jsxs)(i.h3,{id:"ircommonbuilder-extension",children:[(0,a.jsx)(i.code,{children:"IRCommonBuilder"})," extension"]}),"\n",(0,a.jsxs)(i.table,{children:[(0,a.jsx)(i.thead,{children:(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.th,{children:"Method"}),(0,a.jsx)(i.th,{children:"Description"})]})}),(0,a.jsxs)(i.tbody,{children:[(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"WithValidation()"})}),(0,a.jsxs)(i.td,{children:["Registers the validation provider with default ",(0,a.jsx)(i.code,{children:"CqrsValidationOptions"}),"."]})]}),(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"WithValidation(Action)"})}),(0,a.jsx)(i.td,{children:"Registers the validation provider and configures CQRS pipeline validation."})]})]})]}),"\n",(0,a.jsxs)(i.h3,{id:"ifluentvalidationbuilder-extension-methods",children:[(0,a.jsx)(i.code,{children:"IFluentValidationBuilder"})," extension methods"]}),"\n",(0,a.jsxs)(i.table,{children:[(0,a.jsx)(i.thead,{children:(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.th,{children:"Method"}),(0,a.jsx)(i.th,{children:"Description"})]})}),(0,a.jsxs)(i.tbody,{children:[(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"AddValidator()"})}),(0,a.jsxs)(i.td,{children:["Registers a single validator for type ",(0,a.jsx)(i.code,{children:"T"})," with a scoped lifetime."]})]}),(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"AddValidatorsFromAssembly(assembly, lifetime?, filter?, includeInternal?)"})}),(0,a.jsx)(i.td,{children:"Scans the given assembly and registers all validators found."})]}),(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"AddValidatorsFromAssemblies(assemblies, lifetime?, filter?, includeInternal?)"})}),(0,a.jsx)(i.td,{children:"Scans multiple assemblies and registers all validators found."})]}),(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"AddValidatorsFromAssemblyContaining(type, lifetime?, filter?, includeInternal?)"})}),(0,a.jsx)(i.td,{children:"Scans the assembly containing the given type."})]})]})]}),"\n",(0,a.jsx)(i.h3,{id:"cqrsvalidationoptions",children:(0,a.jsx)(i.code,{children:"CqrsValidationOptions"})}),"\n",(0,a.jsxs)(i.table,{children:[(0,a.jsx)(i.thead,{children:(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.th,{children:"Property"}),(0,a.jsx)(i.th,{children:"Default"}),(0,a.jsx)(i.th,{children:"Description"})]})}),(0,a.jsxs)(i.tbody,{children:[(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"ValidateCommands"})}),(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"false"})}),(0,a.jsx)(i.td,{children:"Automatically validate commands before dispatch."})]}),(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"ValidateQueries"})}),(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"false"})}),(0,a.jsx)(i.td,{children:"Automatically validate queries before dispatch."})]})]})]}),"\n",(0,a.jsx)(i.h3,{id:"ivalidationservice",children:(0,a.jsx)(i.code,{children:"IValidationService"})}),"\n",(0,a.jsxs)(i.table,{children:[(0,a.jsx)(i.thead,{children:(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.th,{children:"Method"}),(0,a.jsx)(i.th,{children:"Description"})]})}),(0,a.jsx)(i.tbody,{children:(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"ValidateAsync(target, throwOnFaults?, cancellationToken?)"})}),(0,a.jsxs)(i.td,{children:["Validates ",(0,a.jsx)(i.code,{children:"target"})," and returns a ",(0,a.jsx)(i.code,{children:"ValidationOutcome"}),". Throws ",(0,a.jsx)(i.code,{children:"ValidationException"})," when ",(0,a.jsx)(i.code,{children:"throwOnFaults"})," is ",(0,a.jsx)(i.code,{children:"true"})," and there are failures."]})]})})]}),"\n",(0,a.jsx)(i.h3,{id:"validationoutcome",children:(0,a.jsx)(i.code,{children:"ValidationOutcome"})}),"\n",(0,a.jsxs)(i.p,{children:["Returned by ",(0,a.jsx)(i.code,{children:"IValidationService.ValidateAsync"}),". Contains ",(0,a.jsx)(i.code,{children:"IReadOnlyList Errors"}),"."]}),"\n",(0,a.jsx)(i.h3,{id:"validationfault",children:(0,a.jsx)(i.code,{children:"ValidationFault"})}),"\n",(0,a.jsxs)(i.table,{children:[(0,a.jsx)(i.thead,{children:(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.th,{children:"Property"}),(0,a.jsx)(i.th,{children:"Description"})]})}),(0,a.jsxs)(i.tbody,{children:[(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"PropertyName"})}),(0,a.jsx)(i.td,{children:"The name of the property that failed validation."})]}),(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"Message"})}),(0,a.jsx)(i.td,{children:"The human-readable error message."})]}),(0,a.jsxs)(i.tr,{children:[(0,a.jsx)(i.td,{children:(0,a.jsx)(i.code,{children:"AttemptedValue"})}),(0,a.jsx)(i.td,{children:"The value that was rejected."})]})]})]})]})}function m(e={}){const{wrapper:i}={...(0,t.R)(),...e.components};return i?(0,a.jsx)(i,{...e,children:(0,a.jsx)(h,{...e})}):h(e)}},75783(e,i,n){n.d(i,{A:()=>c});var a=n(30758);const t="container_xjrG",d="label_Y4p8",l="commandRow_FY5I",o="command_m7Qs",r="copyButton_u1GK";var s=n(86070);function c({packageName:e,version:i}){const[n,c]=(0,a.useState)(!1),h=i?`dotnet add package ${e} --version ${i}`:`dotnet add package ${e}`;return(0,s.jsxs)("div",{className:t,children:[(0,s.jsx)("div",{className:d,children:"NuGet Package"}),(0,s.jsxs)("div",{className:l,children:[(0,s.jsx)("code",{className:o,children:h}),(0,s.jsx)("button",{className:r,onClick:()=>{navigator.clipboard.writeText(h),c(!0),setTimeout(()=>c(!1),2e3)},title:"Copy to clipboard",children:n?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,i,n){n.d(i,{R:()=>l,x:()=>o});var a=n(30758);const t={},d=a.createContext(t);function l(e){const i=a.useContext(d);return a.useMemo(function(){return"function"==typeof e?e(i):{...i,...e}},[i,e])}function o(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:l(e.components),a.createElement(d.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/1d75ee8e.be2b544e.js b/website/build/assets/js/1d75ee8e.be2b544e.js new file mode 100644 index 00000000..b8a3a042 --- /dev/null +++ b/website/build/assets/js/1d75ee8e.be2b544e.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[7031],{67384(e,n,r){r.r(n),r.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>u,frontMatter:()=>o,metadata:()=>a,toc:()=>d});var t=r(86070),i=r(81753);r(85363),r(64996);const o={title:"Migration Guide",sidebar_position:3,description:"Step-by-step guide for upgrading RCommon between major versions, covering repository interfaces, builder API changes, soft delete, multitenancy, and .NET framework upgrades."},s="Migration Guide",a={id:"api-reference/migration-guide",title:"Migration Guide",description:"Step-by-step guide for upgrading RCommon between major versions, covering repository interfaces, builder API changes, soft delete, multitenancy, and .NET framework upgrades.",source:"@site/docs/api-reference/migration-guide.mdx",sourceDirName:"api-reference",slug:"/api-reference/migration-guide",permalink:"/docs/next/api-reference/migration-guide",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/api-reference/migration-guide.mdx",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"Migration Guide",sidebar_position:3,description:"Step-by-step guide for upgrading RCommon between major versions, covering repository interfaces, builder API changes, soft delete, multitenancy, and .NET framework upgrades."},sidebar:"docsSidebar",previous:{title:"Changelog",permalink:"/docs/next/api-reference/changelog"}},l={},d=[{value:"General Upgrade Steps",id:"general-upgrade-steps",level:2},{value:"Upgrading Package References",id:"upgrading-package-references",level:2},{value:"Upgrading to Modular Composition",id:"upgrading-to-modular-composition",level:2},{value:"What changed",id:"what-changed",level:3},{value:"What you can now do",id:"what-you-can-now-do",level:3},{value:"Breaking Change Patterns",id:"breaking-change-patterns",level:2},{value:"Repository Interface Changes",id:"repository-interface-changes",level:3},{value:"Builder API Changes",id:"builder-api-changes",level:3},{value:"Entity Base Class Changes",id:"entity-base-class-changes",level:3},{value:"Event Handling Changes",id:"event-handling-changes",level:3},{value:"Mediator Adapter Changes",id:"mediator-adapter-changes",level:3},{value:"Security and Web Changes",id:"security-and-web-changes",level:3},{value:"Multi-Tenancy Migration",id:"multi-tenancy-migration",level:2},{value:"Soft Delete Migration",id:"soft-delete-migration",level:2},{value:"Target Framework Upgrades",id:"target-framework-upgrades",level:2},{value:"Getting Help",id:"getting-help",level:2}];function c(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"migration-guide",children:"Migration Guide"}),"\n",(0,t.jsx)(n.p,{children:"This guide covers the patterns and steps required when upgrading between RCommon versions. Because RCommon follows semantic versioning, breaking changes only appear in major version increments."}),"\n",(0,t.jsxs)(n.p,{children:["For the specific changes in each release, see the ",(0,t.jsx)(n.a,{href:"/docs/next/api-reference/changelog",children:"Changelog"})," or the ",(0,t.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon/releases",children:"GitHub Releases page"}),"."]}),"\n",(0,t.jsx)(n.h2,{id:"general-upgrade-steps",children:"General Upgrade Steps"}),"\n",(0,t.jsx)(n.p,{children:"Before upgrading to a new major version, follow these steps:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsx)(n.li,{children:"Read the release notes for every version between your current version and the target version."}),"\n",(0,t.jsx)(n.li,{children:"Update NuGet packages one major version at a time rather than skipping multiple majors."}),"\n",(0,t.jsx)(n.li,{children:"Address compiler errors first \u2014 these represent API changes that must be resolved."}),"\n",(0,t.jsx)(n.li,{children:"Run the full test suite and address any runtime failures."}),"\n",(0,t.jsx)(n.li,{children:"Verify logging output during integration tests to catch behavioral differences."}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"upgrading-package-references",children:"Upgrading Package References"}),"\n",(0,t.jsxs)(n.p,{children:["Update all ",(0,t.jsx)(n.code,{children:"RCommon.*"})," packages in your project files at once. Because all packages share the same version, mixing versions across packages in the same application is not supported and may cause type resolution failures."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-xml",children:'\n\n\n\n\x3c!-- All RCommon.* packages should share the same version --\x3e\n'})}),"\n",(0,t.jsx)(n.h2,{id:"upgrading-to-modular-composition",children:"Upgrading to Modular Composition"}),"\n",(0,t.jsxs)(n.p,{children:["This release introduces modular composition of ",(0,t.jsx)(n.code,{children:"AddRCommon()"}),". ",(0,t.jsx)(n.strong,{children:"No source changes are required."})," Existing single-call apps continue to compile and behave identically."]}),"\n",(0,t.jsx)(n.h3,{id:"what-changed",children:"What changed"}),"\n",(0,t.jsxs)(n.p,{children:["If you previously relied on ",(0,t.jsx)(n.code,{children:"WithSequentialGuidGenerator"}),", ",(0,t.jsx)(n.code,{children:"WithSimpleGuidGenerator"}),", or ",(0,t.jsx)(n.code,{children:"WithDateTimeSystem"})," throwing ",(0,t.jsx)(n.code,{children:"RCommonBuilderException"})," on a second call, that behavior is now relaxed:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Same implementation type"}),", repeated \u2192 idempotent no-op."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Different implementation type"})," \u2192 still throws ",(0,t.jsx)(n.code,{children:"RCommonBuilderException"})," (with both type names in the message)."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:["Likewise, ",(0,t.jsx)(n.code,{children:"DataStoreFactoryOptions.Register(name)"})," now accepts identical re-registrations:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["Same ",(0,t.jsx)(n.code,{children:"(name, B, C)"})," triple"]})," \u2192 idempotent no-op."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsxs)(n.strong,{children:["Same ",(0,t.jsx)(n.code,{children:"(name, B)"})," with different ",(0,t.jsx)(n.code,{children:"C"})]})," \u2192 still throws ",(0,t.jsx)(n.code,{children:"UnsupportedDataStoreException"}),"."]}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"what-you-can-now-do",children:"What you can now do"}),"\n",(0,t.jsx)(n.p,{children:"Modules can independently configure RCommon features:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'public sealed class OrderingModule : IServiceModule\n{\n public void Configure(IServiceCollection services) =>\n services.AddRCommon()\n .WithPersistence(ef =>\n ef.AddDbContext("Ordering", o => o.UseInMemoryDatabase("ord")));\n}\n\npublic sealed class InventoryModule : IServiceModule\n{\n public void Configure(IServiceCollection services) =>\n services.AddRCommon()\n .WithPersistence(ef =>\n ef.AddDbContext("Inventory", o => o.UseInMemoryDatabase("inv")));\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["Both modules call ",(0,t.jsx)(n.code,{children:"AddRCommon()"})," and ",(0,t.jsx)(n.code,{children:"WithPersistence"}),". The persistence sub-builder's constructor runs exactly once; both ",(0,t.jsx)(n.code,{children:"AddDbContext"})," calls accumulate registrations."]}),"\n",(0,t.jsxs)(n.p,{children:["See ",(0,t.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix and new helper APIs."]}),"\n",(0,t.jsx)(n.h2,{id:"breaking-change-patterns",children:"Breaking Change Patterns"}),"\n",(0,t.jsx)(n.p,{children:"The following sections describe the categories of breaking changes that appear in major RCommon releases and how to address them."}),"\n",(0,t.jsx)(n.h3,{id:"repository-interface-changes",children:"Repository Interface Changes"}),"\n",(0,t.jsx)(n.p,{children:"When repository interfaces gain or lose methods, implement the new contract on any custom repository implementations you have written:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// Before: your custom repository implementing the old interface\npublic class MyCustomRepository : ILinqRepository\n{\n // ...\n}\n\n// After: check what methods were added or removed, then add or remove them\npublic class MyCustomRepository : ILinqRepository\n{\n // Add any new required methods from the updated interface\n public async Task CountAsync(Expression> predicate,\n CancellationToken cancellationToken = default)\n {\n // implementation\n }\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["If you only inject the built-in repositories (such as ",(0,t.jsx)(n.code,{children:"IGraphRepository"})," or ",(0,t.jsx)(n.code,{children:"ILinqRepository"}),") you are not affected by these changes."]}),"\n",(0,t.jsx)(n.h3,{id:"builder-api-changes",children:"Builder API Changes"}),"\n",(0,t.jsxs)(n.p,{children:["The fluent builder methods on ",(0,t.jsx)(n.code,{children:"IRCommonBuilder"})," occasionally move or are renamed. The compiler will surface these as errors on the ",(0,t.jsx)(n.code,{children:"AddRCommon(builder => ...)"})," call in ",(0,t.jsx)(n.code,{children:"Program.cs"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'// Before (hypothetical old API)\nservices.AddRCommon()\n .WithPersistence(ef =>\n {\n ef.UsingEFCore("AppDb", /* ... */);\n });\n\n// After\nservices.AddRCommon()\n .WithPersistence(ef =>\n {\n ef.AddDbContext("AppDb", /* ... */);\n ef.SetDefaultDataStore(o => o.DefaultDataStoreName = "AppDb");\n });\n'})}),"\n",(0,t.jsx)(n.h3,{id:"entity-base-class-changes",children:"Entity Base Class Changes"}),"\n",(0,t.jsxs)(n.p,{children:["If your entities inherit from ",(0,t.jsx)(n.code,{children:"BusinessEntity"})," or ",(0,t.jsx)(n.code,{children:"AuditedEntity"})," and those classes gain new abstract or virtual members, you may need to implement or override them."]}),"\n",(0,t.jsx)(n.p,{children:"The most common change is the addition of new opt-in interfaces:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// Adding soft delete support to an existing entity\npublic class Customer : BusinessEntity, ISoftDelete\n{\n // Add the required property from the ISoftDelete interface\n public bool IsDeleted { get; set; }\n // ... existing properties\n}\n\n// Adding multitenancy to an existing entity\npublic class Customer : BusinessEntity, IMultiTenant\n{\n // Add the required property from the IMultiTenant interface\n public string? TenantId { get; set; }\n // ... existing properties\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["When you add ",(0,t.jsx)(n.code,{children:"ISoftDelete"})," to an entity, you must also add a migration to your EF Core project to add the ",(0,t.jsx)(n.code,{children:"IsDeleted"})," column."]}),"\n",(0,t.jsxs)(n.p,{children:["When you add ",(0,t.jsx)(n.code,{children:"IMultiTenant"}),", you must add a migration to add the ",(0,t.jsx)(n.code,{children:"TenantId"})," column and ensure the ",(0,t.jsx)(n.code,{children:"ITenantIdAccessor"})," is configured in DI."]}),"\n",(0,t.jsx)(n.h3,{id:"event-handling-changes",children:"Event Handling Changes"}),"\n",(0,t.jsxs)(n.p,{children:["If ",(0,t.jsx)(n.code,{children:"IEventBus"}),", ",(0,t.jsx)(n.code,{children:"IEventRouter"}),", ",(0,t.jsx)(n.code,{children:"IEventProducer"}),", or ",(0,t.jsx)(n.code,{children:"ISubscriber"})," change signatures, update your handler registrations and any custom event producer implementations."]}),"\n",(0,t.jsx)(n.p,{children:"The most common pattern is subscriber registration moving from one builder method to another:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// Register event handlers through the event handling builder\nservices.AddRCommon()\n .WithEventHandling(events =>\n {\n events.AddSubscriber();\n events.AddSubscriber();\n });\n"})}),"\n",(0,t.jsx)(n.h3,{id:"mediator-adapter-changes",children:"Mediator Adapter Changes"}),"\n",(0,t.jsxs)(n.p,{children:["If you use ",(0,t.jsx)(n.code,{children:"IMediatorService"})," through the ",(0,t.jsx)(n.code,{children:"RCommon.Mediatr"})," adapter, check whether ",(0,t.jsx)(n.code,{children:"MediatRAdapter"})," or the builder registration changed. The application-facing ",(0,t.jsx)(n.code,{children:"IMediatorService"})," interface is stable; only the adapter registration may change:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// Configure the mediator with the MediatR adapter\nservices.AddRCommon()\n .WithMediator(mediator =>\n {\n mediator.AddRequest();\n mediator.AddNotification();\n mediator.AddLoggingToRequestPipeline();\n mediator.AddUnitOfWorkToRequestPipeline();\n });\n"})}),"\n",(0,t.jsx)(n.h3,{id:"security-and-web-changes",children:"Security and Web Changes"}),"\n",(0,t.jsxs)(n.p,{children:["If your application uses ",(0,t.jsx)(n.code,{children:"ICurrentUser"}),", ",(0,t.jsx)(n.code,{children:"ITenantIdAccessor"}),", or ",(0,t.jsx)(n.code,{children:"ICurrentPrincipalAccessor"}),", verify that the registration method you call still exists:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// For ASP.NET Core web applications (reads from HttpContext.User)\nservices.AddRCommon(config =>\n{\n config.WithClaimsAndPrincipalAccessorForWeb();\n});\n\n// For non-web applications (reads from Thread.CurrentPrincipal)\nservices.AddRCommon(config =>\n{\n config.WithClaimsAndPrincipalAccessor();\n});\n"})}),"\n",(0,t.jsx)(n.h2,{id:"multi-tenancy-migration",children:"Multi-Tenancy Migration"}),"\n",(0,t.jsx)(n.p,{children:"If you are adding multi-tenancy to an existing application that previously had none, follow this sequence:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:["Install ",(0,t.jsx)(n.code,{children:"RCommon.MultiTenancy"})," and a provider package (e.g., ",(0,t.jsx)(n.code,{children:"RCommon.Finbuckle"}),")."]}),"\n",(0,t.jsxs)(n.li,{children:["Add ",(0,t.jsx)(n.code,{children:"IMultiTenant"})," to any entities that should be tenant-scoped."]}),"\n",(0,t.jsxs)(n.li,{children:["Create EF Core migrations for any new ",(0,t.jsx)(n.code,{children:"TenantId"})," columns."]}),"\n",(0,t.jsxs)(n.li,{children:["Register the tenancy provider in ",(0,t.jsx)(n.code,{children:"Program.cs"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:["Populate ",(0,t.jsx)(n.code,{children:"TenantId"})," on existing rows via a data migration if your application is being retrofitted."]}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'// 1. Set up Finbuckle tenant resolution\nbuilder.Services.AddMultiTenant()\n .WithHeaderStrategy("X-Tenant")\n .WithConfigurationStore();\n\n// 2. Register RCommon with Finbuckle\nbuilder.Services.AddRCommon(config =>\n{\n config\n .WithClaimsAndPrincipalAccessorForWeb()\n .WithPersistence(ef =>\n {\n ef.AddDbContext("AppDb", options =>\n options.UseSqlServer(connectionString));\n ef.SetDefaultDataStore(o => o.DefaultDataStoreName = "AppDb");\n })\n .WithMultiTenancy>(mt => { });\n});\n'})}),"\n",(0,t.jsx)(n.h2,{id:"soft-delete-migration",children:"Soft Delete Migration"}),"\n",(0,t.jsx)(n.p,{children:"If you are adding soft delete to entities that previously used physical deletion, follow this sequence:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:["Add ",(0,t.jsx)(n.code,{children:"ISoftDelete"})," to the entity class and add the ",(0,t.jsx)(n.code,{children:"IsDeleted"})," property."]}),"\n",(0,t.jsxs)(n.li,{children:["Create an EF Core migration to add the ",(0,t.jsx)(n.code,{children:"IsDeleted"})," column with a default value of ",(0,t.jsx)(n.code,{children:"false"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:["Verify that any existing raw SQL queries or stored procedures you use are updated to filter on ",(0,t.jsx)(n.code,{children:"IsDeleted = 0"}),"."]}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// Entity before soft delete\npublic class Order : BusinessEntity\n{\n public string ProductName { get; set; }\n}\n\n// Entity after adding soft delete\npublic class Order : BusinessEntity, ISoftDelete\n{\n public string ProductName { get; set; }\n public bool IsDeleted { get; set; }\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["After adding the migration, soft-deleted records are excluded from all repository queries automatically. Code that previously called ",(0,t.jsx)(n.code,{children:"DeleteAsync(order)"})," continues to work \u2014 it performs a physical delete. To perform a soft delete, call ",(0,t.jsx)(n.code,{children:"DeleteAsync(order, isSoftDelete: true)"}),"."]}),"\n",(0,t.jsx)(n.h2,{id:"target-framework-upgrades",children:"Target Framework Upgrades"}),"\n",(0,t.jsxs)(n.p,{children:["RCommon targets .NET 8, .NET 9, and .NET 10. When upgrading your application's target framework, update the ",(0,t.jsx)(n.code,{children:"TargetFramework"})," in your project file and ensure all RCommon packages are updated to a version that supports the new framework:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-xml",children:"\n net10.0\n\n"})}),"\n",(0,t.jsx)(n.p,{children:"Check the package listing on NuGet to confirm which frameworks each package supports. All current RCommon packages support .NET 8, .NET 9, and .NET 10."}),"\n",(0,t.jsx)(n.h2,{id:"getting-help",children:"Getting Help"}),"\n",(0,t.jsx)(n.p,{children:"If you encounter an upgrade issue not covered here:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Search existing ",(0,t.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon/issues",children:"GitHub Issues"})," \u2014 others may have hit the same problem."]}),"\n",(0,t.jsxs)(n.li,{children:["Start a ",(0,t.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon/discussions",children:"GitHub Discussion"})," for questions that are not bugs."]}),"\n",(0,t.jsx)(n.li,{children:"Open a new issue with a minimal reproduction if you believe you have found a bug in the upgrade path."}),"\n"]})]})}function u(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(c,{...e})}):c(e)}},64996(e,n,r){r.d(n,{A:()=>s});r(30758);var t=r(13526);const i="tabItem_jA3u";var o=r(86070);function s({children:e,hidden:n,className:r}){return(0,o.jsx)("div",{role:"tabpanel",className:(0,t.A)(i,r),hidden:n,children:e})}},85363(e,n,r){r.d(n,{A:()=>w});var t=r(30758),i=r(13526),o=r(32416),s=r(25557),a=r(85924),l=r(64493),d=r(10274),c=r(44118);function u(e){return t.Children.toArray(e).filter(e=>"\n"!==e).map(e=>{if(!e||(0,t.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)})?.filter(Boolean)??[]}function h(e){const{values:n,children:r}=e;return(0,t.useMemo)(()=>{const e=n??function(e){return u(e).map(({props:{value:e,label:n,attributes:r,default:t}})=>({value:e,label:n,attributes:r,default:t}))}(r);return function(e){const n=(0,d.X)(e,(e,n)=>e.value===n.value);if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map(e=>e.value).join(", ")}" found in . Every value needs to be unique.`)}(e),e},[n,r])}function p({value:e,tabValues:n}){return n.some(n=>n.value===e)}function m({queryString:e=!1,groupId:n}){const r=(0,s.W6)(),i=function({queryString:e=!1,groupId:n}){if("string"==typeof e)return e;if(!1===e)return null;if(!0===e&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:e,groupId:n});return[(0,l.aZ)(i),(0,t.useCallback)(e=>{if(!i)return;const n=new URLSearchParams(r.location.search);n.set(i,e),r.replace({...r.location,search:n.toString()})},[i,r])]}function g(e){const{defaultValue:n,queryString:r=!1,groupId:i}=e,o=h(e),[s,l]=(0,t.useState)(()=>function({defaultValue:e,tabValues:n}){if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(e){if(!p({value:e,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${e}" but none of its children has the corresponding value. Available values are: ${n.map(e=>e.value).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return e}const r=n.find(e=>e.default)??n[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:n,tabValues:o})),[d,u]=m({queryString:r,groupId:i}),[g,f]=function({groupId:e}){const n=function(e){return e?`docusaurus.tab.${e}`:null}(e),[r,i]=(0,c.Dv)(n);return[r,(0,t.useCallback)(e=>{n&&i.set(e)},[n,i])]}({groupId:i}),x=(()=>{const e=d??g;return p({value:e,tabValues:o})?e:null})();(0,a.A)(()=>{x&&l(x)},[x]);return{selectedValue:s,selectValue:(0,t.useCallback)(e=>{if(!p({value:e,tabValues:o}))throw new Error(`Can't select invalid tab value=${e}`);l(e),u(e),f(e)},[u,f,o]),tabValues:o}}var f=r(81264);const x="tabList_TfjZ",j="tabItem_vVs9";var b=r(86070);function v({className:e,block:n,selectedValue:r,selectValue:t,tabValues:s}){const a=[],{blockElementScrollPositionUntilNextRender:l}=(0,o.a_)(),d=e=>{const n=e.currentTarget,i=a.indexOf(n),o=s[i].value;o!==r&&(l(n),t(o))},c=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const r=a.indexOf(e.currentTarget)+1;n=a[r]??a[0];break}case"ArrowLeft":{const r=a.indexOf(e.currentTarget)-1;n=a[r]??a[a.length-1];break}}n?.focus()};return(0,b.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,i.A)("tabs",{"tabs--block":n},e),children:s.map(({value:e,label:n,attributes:t})=>(0,b.jsx)("li",{role:"tab",tabIndex:r===e?0:-1,"aria-selected":r===e,ref:e=>a.push(e),onKeyDown:c,onClick:d,...t,className:(0,i.A)("tabs__item",j,t?.className,{"tabs__item--active":r===e}),children:n??e},e))})}function y({lazy:e,children:n,selectedValue:r}){const i=(Array.isArray(n)?n:[n]).filter(Boolean);if(e){const e=i.find(e=>e.props.value===r);return e?(0,t.cloneElement)(e,{className:"margin-top--md"}):null}return(0,b.jsx)("div",{className:"margin-top--md",children:i.map((e,n)=>(0,t.cloneElement)(e,{key:n,hidden:e.props.value!==r}))})}function C(e){const n=g(e);return(0,b.jsxs)("div",{className:(0,i.A)("tabs-container",x),children:[(0,b.jsx)(v,{...n,...e}),(0,b.jsx)(y,{...n,...e})]})}function w(e){const n=(0,f.A)();return(0,b.jsx)(C,{...e,children:u(e.children)},String(n))}},81753(e,n,r){r.d(n,{R:()=>s,x:()=>a});var t=r(30758);const i={},o=t.createContext(i);function s(e){const n=t.useContext(o);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),t.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/1d75ee8e.d55ce914.js b/website/build/assets/js/1d75ee8e.d55ce914.js deleted file mode 100644 index 46018c81..00000000 --- a/website/build/assets/js/1d75ee8e.d55ce914.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[7031],{67384(e,n,r){r.r(n),r.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>u,frontMatter:()=>a,metadata:()=>o,toc:()=>d});var t=r(86070),i=r(81753);r(85363),r(64996);const a={title:"Migration Guide",sidebar_position:3,description:"Step-by-step guide for upgrading RCommon between major versions, covering repository interfaces, builder API changes, soft delete, multitenancy, and .NET framework upgrades."},s="Migration Guide",o={id:"api-reference/migration-guide",title:"Migration Guide",description:"Step-by-step guide for upgrading RCommon between major versions, covering repository interfaces, builder API changes, soft delete, multitenancy, and .NET framework upgrades.",source:"@site/docs/api-reference/migration-guide.mdx",sourceDirName:"api-reference",slug:"/api-reference/migration-guide",permalink:"/docs/next/api-reference/migration-guide",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/api-reference/migration-guide.mdx",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"Migration Guide",sidebar_position:3,description:"Step-by-step guide for upgrading RCommon between major versions, covering repository interfaces, builder API changes, soft delete, multitenancy, and .NET framework upgrades."},sidebar:"docsSidebar",previous:{title:"Changelog",permalink:"/docs/next/api-reference/changelog"}},l={},d=[{value:"General Upgrade Steps",id:"general-upgrade-steps",level:2},{value:"Upgrading Package References",id:"upgrading-package-references",level:2},{value:"Breaking Change Patterns",id:"breaking-change-patterns",level:2},{value:"Repository Interface Changes",id:"repository-interface-changes",level:3},{value:"Builder API Changes",id:"builder-api-changes",level:3},{value:"Entity Base Class Changes",id:"entity-base-class-changes",level:3},{value:"Event Handling Changes",id:"event-handling-changes",level:3},{value:"Mediator Adapter Changes",id:"mediator-adapter-changes",level:3},{value:"Security and Web Changes",id:"security-and-web-changes",level:3},{value:"Multi-Tenancy Migration",id:"multi-tenancy-migration",level:2},{value:"Soft Delete Migration",id:"soft-delete-migration",level:2},{value:"Target Framework Upgrades",id:"target-framework-upgrades",level:2},{value:"Getting Help",id:"getting-help",level:2}];function c(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",ul:"ul",...(0,i.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"migration-guide",children:"Migration Guide"}),"\n",(0,t.jsx)(n.p,{children:"This guide covers the patterns and steps required when upgrading between RCommon versions. Because RCommon follows semantic versioning, breaking changes only appear in major version increments."}),"\n",(0,t.jsxs)(n.p,{children:["For the specific changes in each release, see the ",(0,t.jsx)(n.a,{href:"/docs/next/api-reference/changelog",children:"Changelog"})," or the ",(0,t.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon/releases",children:"GitHub Releases page"}),"."]}),"\n",(0,t.jsx)(n.h2,{id:"general-upgrade-steps",children:"General Upgrade Steps"}),"\n",(0,t.jsx)(n.p,{children:"Before upgrading to a new major version, follow these steps:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsx)(n.li,{children:"Read the release notes for every version between your current version and the target version."}),"\n",(0,t.jsx)(n.li,{children:"Update NuGet packages one major version at a time rather than skipping multiple majors."}),"\n",(0,t.jsx)(n.li,{children:"Address compiler errors first \u2014 these represent API changes that must be resolved."}),"\n",(0,t.jsx)(n.li,{children:"Run the full test suite and address any runtime failures."}),"\n",(0,t.jsx)(n.li,{children:"Verify logging output during integration tests to catch behavioral differences."}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"upgrading-package-references",children:"Upgrading Package References"}),"\n",(0,t.jsxs)(n.p,{children:["Update all ",(0,t.jsx)(n.code,{children:"RCommon.*"})," packages in your project files at once. Because all packages share the same version, mixing versions across packages in the same application is not supported and may cause type resolution failures."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-xml",children:'\r\n\r\n\r\n\r\n\x3c!-- All RCommon.* packages should share the same version --\x3e\n'})}),"\n",(0,t.jsx)(n.h2,{id:"breaking-change-patterns",children:"Breaking Change Patterns"}),"\n",(0,t.jsx)(n.p,{children:"The following sections describe the categories of breaking changes that appear in major RCommon releases and how to address them."}),"\n",(0,t.jsx)(n.h3,{id:"repository-interface-changes",children:"Repository Interface Changes"}),"\n",(0,t.jsx)(n.p,{children:"When repository interfaces gain or lose methods, implement the new contract on any custom repository implementations you have written:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// Before: your custom repository implementing the old interface\r\npublic class MyCustomRepository : ILinqRepository\r\n{\r\n // ...\r\n}\r\n\r\n// After: check what methods were added or removed, then add or remove them\r\npublic class MyCustomRepository : ILinqRepository\r\n{\r\n // Add any new required methods from the updated interface\r\n public async Task CountAsync(Expression> predicate,\r\n CancellationToken cancellationToken = default)\r\n {\r\n // implementation\r\n }\r\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["If you only inject the built-in repositories (such as ",(0,t.jsx)(n.code,{children:"IGraphRepository"})," or ",(0,t.jsx)(n.code,{children:"ILinqRepository"}),") you are not affected by these changes."]}),"\n",(0,t.jsx)(n.h3,{id:"builder-api-changes",children:"Builder API Changes"}),"\n",(0,t.jsxs)(n.p,{children:["The fluent builder methods on ",(0,t.jsx)(n.code,{children:"IRCommonBuilder"})," occasionally move or are renamed. The compiler will surface these as errors on the ",(0,t.jsx)(n.code,{children:"AddRCommon(builder => ...)"})," call in ",(0,t.jsx)(n.code,{children:"Program.cs"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'// Before (hypothetical old API)\r\nservices.AddRCommon()\r\n .WithPersistence(ef =>\r\n {\r\n ef.UsingEFCore("AppDb", /* ... */);\r\n });\r\n\r\n// After\r\nservices.AddRCommon()\r\n .WithPersistence(ef =>\r\n {\r\n ef.AddDbContext("AppDb", /* ... */);\r\n ef.SetDefaultDataStore(o => o.DefaultDataStoreName = "AppDb");\r\n });\n'})}),"\n",(0,t.jsx)(n.h3,{id:"entity-base-class-changes",children:"Entity Base Class Changes"}),"\n",(0,t.jsxs)(n.p,{children:["If your entities inherit from ",(0,t.jsx)(n.code,{children:"BusinessEntity"})," or ",(0,t.jsx)(n.code,{children:"AuditedEntity"})," and those classes gain new abstract or virtual members, you may need to implement or override them."]}),"\n",(0,t.jsx)(n.p,{children:"The most common change is the addition of new opt-in interfaces:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// Adding soft delete support to an existing entity\r\npublic class Customer : BusinessEntity, ISoftDelete\r\n{\r\n // Add the required property from the ISoftDelete interface\r\n public bool IsDeleted { get; set; }\r\n // ... existing properties\r\n}\r\n\r\n// Adding multitenancy to an existing entity\r\npublic class Customer : BusinessEntity, IMultiTenant\r\n{\r\n // Add the required property from the IMultiTenant interface\r\n public string? TenantId { get; set; }\r\n // ... existing properties\r\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["When you add ",(0,t.jsx)(n.code,{children:"ISoftDelete"})," to an entity, you must also add a migration to your EF Core project to add the ",(0,t.jsx)(n.code,{children:"IsDeleted"})," column."]}),"\n",(0,t.jsxs)(n.p,{children:["When you add ",(0,t.jsx)(n.code,{children:"IMultiTenant"}),", you must add a migration to add the ",(0,t.jsx)(n.code,{children:"TenantId"})," column and ensure the ",(0,t.jsx)(n.code,{children:"ITenantIdAccessor"})," is configured in DI."]}),"\n",(0,t.jsx)(n.h3,{id:"event-handling-changes",children:"Event Handling Changes"}),"\n",(0,t.jsxs)(n.p,{children:["If ",(0,t.jsx)(n.code,{children:"IEventBus"}),", ",(0,t.jsx)(n.code,{children:"IEventRouter"}),", ",(0,t.jsx)(n.code,{children:"IEventProducer"}),", or ",(0,t.jsx)(n.code,{children:"ISubscriber"})," change signatures, update your handler registrations and any custom event producer implementations."]}),"\n",(0,t.jsx)(n.p,{children:"The most common pattern is subscriber registration moving from one builder method to another:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// Register event handlers through the event handling builder\r\nservices.AddRCommon()\r\n .WithEventHandling(events =>\r\n {\r\n events.AddSubscriber();\r\n events.AddSubscriber();\r\n });\n"})}),"\n",(0,t.jsx)(n.h3,{id:"mediator-adapter-changes",children:"Mediator Adapter Changes"}),"\n",(0,t.jsxs)(n.p,{children:["If you use ",(0,t.jsx)(n.code,{children:"IMediatorService"})," through the ",(0,t.jsx)(n.code,{children:"RCommon.Mediatr"})," adapter, check whether ",(0,t.jsx)(n.code,{children:"MediatRAdapter"})," or the builder registration changed. The application-facing ",(0,t.jsx)(n.code,{children:"IMediatorService"})," interface is stable; only the adapter registration may change:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// Configure the mediator with the MediatR adapter\r\nservices.AddRCommon()\r\n .WithMediator(mediator =>\r\n {\r\n mediator.AddRequest();\r\n mediator.AddNotification();\r\n mediator.AddLoggingToRequestPipeline();\r\n mediator.AddUnitOfWorkToRequestPipeline();\r\n });\n"})}),"\n",(0,t.jsx)(n.h3,{id:"security-and-web-changes",children:"Security and Web Changes"}),"\n",(0,t.jsxs)(n.p,{children:["If your application uses ",(0,t.jsx)(n.code,{children:"ICurrentUser"}),", ",(0,t.jsx)(n.code,{children:"ITenantIdAccessor"}),", or ",(0,t.jsx)(n.code,{children:"ICurrentPrincipalAccessor"}),", verify that the registration method you call still exists:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// For ASP.NET Core web applications (reads from HttpContext.User)\r\nservices.AddRCommon(config =>\r\n{\r\n config.WithClaimsAndPrincipalAccessorForWeb();\r\n});\r\n\r\n// For non-web applications (reads from Thread.CurrentPrincipal)\r\nservices.AddRCommon(config =>\r\n{\r\n config.WithClaimsAndPrincipalAccessor();\r\n});\n"})}),"\n",(0,t.jsx)(n.h2,{id:"multi-tenancy-migration",children:"Multi-Tenancy Migration"}),"\n",(0,t.jsx)(n.p,{children:"If you are adding multi-tenancy to an existing application that previously had none, follow this sequence:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:["Install ",(0,t.jsx)(n.code,{children:"RCommon.MultiTenancy"})," and a provider package (e.g., ",(0,t.jsx)(n.code,{children:"RCommon.Finbuckle"}),")."]}),"\n",(0,t.jsxs)(n.li,{children:["Add ",(0,t.jsx)(n.code,{children:"IMultiTenant"})," to any entities that should be tenant-scoped."]}),"\n",(0,t.jsxs)(n.li,{children:["Create EF Core migrations for any new ",(0,t.jsx)(n.code,{children:"TenantId"})," columns."]}),"\n",(0,t.jsxs)(n.li,{children:["Register the tenancy provider in ",(0,t.jsx)(n.code,{children:"Program.cs"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:["Populate ",(0,t.jsx)(n.code,{children:"TenantId"})," on existing rows via a data migration if your application is being retrofitted."]}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'// 1. Set up Finbuckle tenant resolution\r\nbuilder.Services.AddMultiTenant()\r\n .WithHeaderStrategy("X-Tenant")\r\n .WithConfigurationStore();\r\n\r\n// 2. Register RCommon with Finbuckle\r\nbuilder.Services.AddRCommon(config =>\r\n{\r\n config\r\n .WithClaimsAndPrincipalAccessorForWeb()\r\n .WithPersistence(ef =>\r\n {\r\n ef.AddDbContext("AppDb", options =>\r\n options.UseSqlServer(connectionString));\r\n ef.SetDefaultDataStore(o => o.DefaultDataStoreName = "AppDb");\r\n })\r\n .WithMultiTenancy>(mt => { });\r\n});\n'})}),"\n",(0,t.jsx)(n.h2,{id:"soft-delete-migration",children:"Soft Delete Migration"}),"\n",(0,t.jsx)(n.p,{children:"If you are adding soft delete to entities that previously used physical deletion, follow this sequence:"}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:["Add ",(0,t.jsx)(n.code,{children:"ISoftDelete"})," to the entity class and add the ",(0,t.jsx)(n.code,{children:"IsDeleted"})," property."]}),"\n",(0,t.jsxs)(n.li,{children:["Create an EF Core migration to add the ",(0,t.jsx)(n.code,{children:"IsDeleted"})," column with a default value of ",(0,t.jsx)(n.code,{children:"false"}),"."]}),"\n",(0,t.jsxs)(n.li,{children:["Verify that any existing raw SQL queries or stored procedures you use are updated to filter on ",(0,t.jsx)(n.code,{children:"IsDeleted = 0"}),"."]}),"\n"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// Entity before soft delete\r\npublic class Order : BusinessEntity\r\n{\r\n public string ProductName { get; set; }\r\n}\r\n\r\n// Entity after adding soft delete\r\npublic class Order : BusinessEntity, ISoftDelete\r\n{\r\n public string ProductName { get; set; }\r\n public bool IsDeleted { get; set; }\r\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["After adding the migration, soft-deleted records are excluded from all repository queries automatically. Code that previously called ",(0,t.jsx)(n.code,{children:"DeleteAsync(order)"})," continues to work \u2014 it performs a physical delete. To perform a soft delete, call ",(0,t.jsx)(n.code,{children:"DeleteAsync(order, isSoftDelete: true)"}),"."]}),"\n",(0,t.jsx)(n.h2,{id:"target-framework-upgrades",children:"Target Framework Upgrades"}),"\n",(0,t.jsxs)(n.p,{children:["RCommon targets .NET 8, .NET 9, and .NET 10. When upgrading your application's target framework, update the ",(0,t.jsx)(n.code,{children:"TargetFramework"})," in your project file and ensure all RCommon packages are updated to a version that supports the new framework:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-xml",children:"\r\n net10.0\r\n\n"})}),"\n",(0,t.jsx)(n.p,{children:"Check the package listing on NuGet to confirm which frameworks each package supports. All current RCommon packages support .NET 8, .NET 9, and .NET 10."}),"\n",(0,t.jsx)(n.h2,{id:"getting-help",children:"Getting Help"}),"\n",(0,t.jsx)(n.p,{children:"If you encounter an upgrade issue not covered here:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["Search existing ",(0,t.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon/issues",children:"GitHub Issues"})," \u2014 others may have hit the same problem."]}),"\n",(0,t.jsxs)(n.li,{children:["Start a ",(0,t.jsx)(n.a,{href:"https://github.com/RCommon-Team/RCommon/discussions",children:"GitHub Discussion"})," for questions that are not bugs."]}),"\n",(0,t.jsx)(n.li,{children:"Open a new issue with a minimal reproduction if you believe you have found a bug in the upgrade path."}),"\n"]})]})}function u(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(c,{...e})}):c(e)}},64996(e,n,r){r.d(n,{A:()=>s});r(30758);var t=r(13526);const i="tabItem_jA3u";var a=r(86070);function s({children:e,hidden:n,className:r}){return(0,a.jsx)("div",{role:"tabpanel",className:(0,t.A)(i,r),hidden:n,children:e})}},85363(e,n,r){r.d(n,{A:()=>I});var t=r(30758),i=r(13526),a=r(32416),s=r(25557),o=r(85924),l=r(64493),d=r(10274),c=r(44118);function u(e){return t.Children.toArray(e).filter(e=>"\n"!==e).map(e=>{if(!e||(0,t.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)})?.filter(Boolean)??[]}function h(e){const{values:n,children:r}=e;return(0,t.useMemo)(()=>{const e=n??function(e){return u(e).map(({props:{value:e,label:n,attributes:r,default:t}})=>({value:e,label:n,attributes:r,default:t}))}(r);return function(e){const n=(0,d.X)(e,(e,n)=>e.value===n.value);if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map(e=>e.value).join(", ")}" found in . Every value needs to be unique.`)}(e),e},[n,r])}function p({value:e,tabValues:n}){return n.some(n=>n.value===e)}function m({queryString:e=!1,groupId:n}){const r=(0,s.W6)(),i=function({queryString:e=!1,groupId:n}){if("string"==typeof e)return e;if(!1===e)return null;if(!0===e&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:e,groupId:n});return[(0,l.aZ)(i),(0,t.useCallback)(e=>{if(!i)return;const n=new URLSearchParams(r.location.search);n.set(i,e),r.replace({...r.location,search:n.toString()})},[i,r])]}function g(e){const{defaultValue:n,queryString:r=!1,groupId:i}=e,a=h(e),[s,l]=(0,t.useState)(()=>function({defaultValue:e,tabValues:n}){if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(e){if(!p({value:e,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${e}" but none of its children has the corresponding value. Available values are: ${n.map(e=>e.value).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return e}const r=n.find(e=>e.default)??n[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:n,tabValues:a})),[d,u]=m({queryString:r,groupId:i}),[g,f]=function({groupId:e}){const n=function(e){return e?`docusaurus.tab.${e}`:null}(e),[r,i]=(0,c.Dv)(n);return[r,(0,t.useCallback)(e=>{n&&i.set(e)},[n,i])]}({groupId:i}),x=(()=>{const e=d??g;return p({value:e,tabValues:a})?e:null})();(0,o.A)(()=>{x&&l(x)},[x]);return{selectedValue:s,selectValue:(0,t.useCallback)(e=>{if(!p({value:e,tabValues:a}))throw new Error(`Can't select invalid tab value=${e}`);l(e),u(e),f(e)},[u,f,a]),tabValues:a}}var f=r(81264);const x="tabList_TfjZ",b="tabItem_vVs9";var j=r(86070);function v({className:e,block:n,selectedValue:r,selectValue:t,tabValues:s}){const o=[],{blockElementScrollPositionUntilNextRender:l}=(0,a.a_)(),d=e=>{const n=e.currentTarget,i=o.indexOf(n),a=s[i].value;a!==r&&(l(n),t(a))},c=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const r=o.indexOf(e.currentTarget)+1;n=o[r]??o[0];break}case"ArrowLeft":{const r=o.indexOf(e.currentTarget)-1;n=o[r]??o[o.length-1];break}}n?.focus()};return(0,j.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,i.A)("tabs",{"tabs--block":n},e),children:s.map(({value:e,label:n,attributes:t})=>(0,j.jsx)("li",{role:"tab",tabIndex:r===e?0:-1,"aria-selected":r===e,ref:e=>o.push(e),onKeyDown:c,onClick:d,...t,className:(0,i.A)("tabs__item",b,t?.className,{"tabs__item--active":r===e}),children:n??e},e))})}function y({lazy:e,children:n,selectedValue:r}){const i=(Array.isArray(n)?n:[n]).filter(Boolean);if(e){const e=i.find(e=>e.props.value===r);return e?(0,t.cloneElement)(e,{className:"margin-top--md"}):null}return(0,j.jsx)("div",{className:"margin-top--md",children:i.map((e,n)=>(0,t.cloneElement)(e,{key:n,hidden:e.props.value!==r}))})}function C(e){const n=g(e);return(0,j.jsxs)("div",{className:(0,i.A)("tabs-container",x),children:[(0,j.jsx)(v,{...n,...e}),(0,j.jsx)(y,{...n,...e})]})}function I(e){const n=(0,f.A)();return(0,j.jsx)(C,{...e,children:u(e.children)},String(n))}},81753(e,n,r){r.d(n,{R:()=>s,x:()=>o});var t=r(30758);const i={},a=t.createContext(i);function s(e){const n=t.useContext(a);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),t.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/217dc876.22bbb9de.js b/website/build/assets/js/217dc876.22bbb9de.js deleted file mode 100644 index c05c3912..00000000 --- a/website/build/assets/js/217dc876.22bbb9de.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[8279],{37216(e,n,r){r.r(n),r.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>h,frontMatter:()=>a,metadata:()=>o,toc:()=>c});var i=r(86070),t=r(81753),d=r(75783);const a={title:"GUID Generation",sidebar_position:3,description:"RCommon's IGuidGenerator abstraction for testable GUID creation \u2014 sequential GUIDs for SQL Server, MySQL, PostgreSQL, and Oracle to reduce index fragmentation."},s="GUID Generation",o={id:"core-concepts/guid-generation",title:"GUID Generation",description:"RCommon's IGuidGenerator abstraction for testable GUID creation \u2014 sequential GUIDs for SQL Server, MySQL, PostgreSQL, and Oracle to reduce index fragmentation.",source:"@site/docs/core-concepts/guid-generation.mdx",sourceDirName:"core-concepts",slug:"/core-concepts/guid-generation",permalink:"/docs/next/core-concepts/guid-generation",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/core-concepts/guid-generation.mdx",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"GUID Generation",sidebar_position:3,description:"RCommon's IGuidGenerator abstraction for testable GUID creation \u2014 sequential GUIDs for SQL Server, MySQL, PostgreSQL, and Oracle to reduce index fragmentation."},sidebar:"docsSidebar",previous:{title:"Guards & Validation",permalink:"/docs/next/core-concepts/guards"},next:{title:"System Time Abstraction",permalink:"/docs/next/core-concepts/system-time"}},l={},c=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Simple (random) GUIDs",id:"simple-random-guids",level:3},{value:"Sequential GUIDs",id:"sequential-guids",level:3},{value:"Usage",id:"usage",level:2},{value:"Generating a specific sequential GUID type at runtime",id:"generating-a-specific-sequential-guid-type-at-runtime",level:3},{value:"In unit tests",id:"in-unit-tests",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"IGuidGenerator",id:"iguidgenerator",level:3},{value:"SimpleGuidGenerator",id:"simpleguidgenerator",level:3},{value:"SequentialGuidGenerator",id:"sequentialguidgenerator",level:3},{value:"SequentialGuidGeneratorOptions",id:"sequentialguidgeneratoroptions",level:3},{value:"SequentialGuidType enum",id:"sequentialguidtype-enum",level:3}];function u(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"guid-generation",children:"GUID Generation"}),"\n",(0,i.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsxs)(n.p,{children:["RCommon abstracts GUID generation behind an ",(0,i.jsx)(n.code,{children:"IGuidGenerator"})," interface for two reasons:"]}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Testability."})," Code that calls ",(0,i.jsx)(n.code,{children:"Guid.NewGuid()"})," directly cannot be controlled in unit tests. Injecting ",(0,i.jsx)(n.code,{children:"IGuidGenerator"})," lets tests substitute a predictable implementation."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Database performance."})," Standard random GUIDs cause index fragmentation in clustered indexes because their values are not monotonically increasing. Sequential GUIDs embed a timestamp in the byte layout so new rows are always appended at the end of the index."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["RCommon ships two implementations out of the box: ",(0,i.jsx)(n.code,{children:"SimpleGuidGenerator"})," and ",(0,i.jsx)(n.code,{children:"SequentialGuidGenerator"}),". You pick one during application configuration; only one generator may be registered per application."]}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(d.A,{packageName:"RCommon.Core"}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["Configure GUID generation inside the ",(0,i.jsx)(n.code,{children:"AddRCommon()"})," builder chain."]}),"\n",(0,i.jsx)(n.h3,{id:"simple-random-guids",children:"Simple (random) GUIDs"}),"\n",(0,i.jsxs)(n.p,{children:["Uses ",(0,i.jsx)(n.code,{children:"Guid.NewGuid()"})," internally. Suitable for non-database use cases or when the underlying database handles GUID ordering itself (e.g., ",(0,i.jsx)(n.code,{children:"NEWSEQUENTIALID()"})," in SQL Server)."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithSimpleGuidGenerator();\n"})}),"\n",(0,i.jsx)(n.h3,{id:"sequential-guids",children:"Sequential GUIDs"}),"\n",(0,i.jsxs)(n.p,{children:["Generates GUIDs with an embedded timestamp so that successive calls produce values that sort in the correct order for a given database platform. The byte layout varies by platform; choose the appropriate ",(0,i.jsx)(n.code,{children:"SequentialGuidType"})," for your database:"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:(0,i.jsx)(n.code,{children:"SequentialGuidType"})}),(0,i.jsx)(n.th,{children:"Database"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"SequentialAsString"})}),(0,i.jsx)(n.td,{children:"MySQL, PostgreSQL"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"SequentialAsBinary"})}),(0,i.jsx)(n.td,{children:"Oracle"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"SequentialAtEnd"})}),(0,i.jsx)(n.td,{children:"SQL Server (default)"})]})]})]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithSequentialGuidGenerator(options =>\r\n {\r\n options.DefaultSequentialGuidType = SequentialGuidType.SequentialAtEnd;\r\n });\n"})}),"\n",(0,i.jsxs)(n.p,{children:["When ",(0,i.jsx)(n.code,{children:"DefaultSequentialGuidType"})," is left ",(0,i.jsx)(n.code,{children:"null"}),", the generator defaults to ",(0,i.jsx)(n.code,{children:"SequentialAtEnd"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsxs)(n.p,{children:["Inject ",(0,i.jsx)(n.code,{children:"IGuidGenerator"})," wherever you need to create a new identifier:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\r\n\r\npublic class OrderService\r\n{\r\n private readonly IGuidGenerator _guidGenerator;\r\n\r\n public OrderService(IGuidGenerator guidGenerator)\r\n {\r\n _guidGenerator = guidGenerator;\r\n }\r\n\r\n public Order CreateOrder(CustomerId customerId)\r\n {\r\n var orderId = _guidGenerator.Create();\r\n return new Order(orderId, customerId);\r\n }\r\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"generating-a-specific-sequential-guid-type-at-runtime",children:"Generating a specific sequential GUID type at runtime"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"SequentialGuidGenerator"})," exposes an overload that accepts a ",(0,i.jsx)(n.code,{children:"SequentialGuidType"})," directly, which is useful when a single application works with multiple databases:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"// Only available when you have a reference to SequentialGuidGenerator directly\r\nvar generator = serviceProvider.GetRequiredService() as SequentialGuidGenerator;\r\nvar guid = generator?.Create(SequentialGuidType.SequentialAsString);\n"})}),"\n",(0,i.jsxs)(n.p,{children:["For most scenarios, rely on the default configured via ",(0,i.jsx)(n.code,{children:"WithSequentialGuidGenerator"})," and inject ",(0,i.jsx)(n.code,{children:"IGuidGenerator"})," \u2014 the concrete type is an implementation detail."]}),"\n",(0,i.jsx)(n.h3,{id:"in-unit-tests",children:"In unit tests"}),"\n",(0,i.jsxs)(n.p,{children:["Substitute ",(0,i.jsx)(n.code,{children:"IGuidGenerator"})," with a test double that returns a known value:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'public class FakeGuidGenerator : IGuidGenerator\r\n{\r\n private readonly Guid _value;\r\n\r\n public FakeGuidGenerator(Guid value)\r\n {\r\n _value = value;\r\n }\r\n\r\n public Guid Create() => _value;\r\n}\r\n\r\n// In a test\r\nvar knownId = Guid.Parse("00000000-0000-0000-0000-000000000001");\r\nvar service = new OrderService(new FakeGuidGenerator(knownId));\r\nvar order = service.CreateOrder(customerId);\r\nAssert.Equal(knownId, order.Id);\n'})}),"\n",(0,i.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsx)(n.h3,{id:"iguidgenerator",children:(0,i.jsx)(n.code,{children:"IGuidGenerator"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Member"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Guid Create()"})}),(0,i.jsxs)(n.td,{children:["Creates and returns a new ",(0,i.jsx)(n.code,{children:"Guid"}),"."]})]})})]}),"\n",(0,i.jsx)(n.h3,{id:"simpleguidgenerator",children:(0,i.jsx)(n.code,{children:"SimpleGuidGenerator"})}),"\n",(0,i.jsxs)(n.p,{children:["Registered by ",(0,i.jsx)(n.code,{children:"WithSimpleGuidGenerator()"}),". Calls ",(0,i.jsx)(n.code,{children:"Guid.NewGuid()"})," on every invocation. Scoped lifetime."]}),"\n",(0,i.jsx)(n.h3,{id:"sequentialguidgenerator",children:(0,i.jsx)(n.code,{children:"SequentialGuidGenerator"})}),"\n",(0,i.jsxs)(n.p,{children:["Registered by ",(0,i.jsx)(n.code,{children:"WithSequentialGuidGenerator(...)"}),". Transient lifetime. Generates a GUID composed of 10 bytes of cryptographically random data and a 6-byte millisecond-resolution timestamp. Byte layout is controlled by ",(0,i.jsx)(n.code,{children:"SequentialGuidType"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"sequentialguidgeneratoroptions",children:(0,i.jsx)(n.code,{children:"SequentialGuidGeneratorOptions"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Property"}),(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Default"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"DefaultSequentialGuidType"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"SequentialGuidType?"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"null"})," (resolves to ",(0,i.jsx)(n.code,{children:"SequentialAtEnd"}),")"]}),(0,i.jsxs)(n.td,{children:["The byte layout strategy used when ",(0,i.jsx)(n.code,{children:"Create()"})," is called without an explicit type."]})]})})]}),"\n",(0,i.jsxs)(n.h3,{id:"sequentialguidtype-enum",children:[(0,i.jsx)(n.code,{children:"SequentialGuidType"})," enum"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Value"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"SequentialAsString"})}),(0,i.jsx)(n.td,{children:"Timestamp at start; sequential when formatted as a string. Use with MySQL, PostgreSQL."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"SequentialAsBinary"})}),(0,i.jsx)(n.td,{children:"Timestamp at start; sequential when compared as a byte array. Use with Oracle."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"SequentialAtEnd"})}),(0,i.jsx)(n.td,{children:"Timestamp at end of the Data4 block. Use with SQL Server."})]})]})]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(u,{...e})}):u(e)}},75783(e,n,r){r.d(n,{A:()=>c});var i=r(30758);const t="container_xjrG",d="label_Y4p8",a="commandRow_FY5I",s="command_m7Qs",o="copyButton_u1GK";var l=r(86070);function c({packageName:e,version:n}){const[r,c]=(0,i.useState)(!1),u=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,l.jsxs)("div",{className:t,children:[(0,l.jsx)("div",{className:d,children:"NuGet Package"}),(0,l.jsxs)("div",{className:a,children:[(0,l.jsx)("code",{className:s,children:u}),(0,l.jsx)("button",{className:o,onClick:()=>{navigator.clipboard.writeText(u),c(!0),setTimeout(()=>c(!1),2e3)},title:"Copy to clipboard",children:r?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,r){r.d(n,{R:()=>a,x:()=>s});var i=r(30758);const t={},d=i.createContext(t);function a(e){const n=i.useContext(d);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:a(e.components),i.createElement(d.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/217dc876.30c8f19d.js b/website/build/assets/js/217dc876.30c8f19d.js new file mode 100644 index 00000000..b9e1d2a3 --- /dev/null +++ b/website/build/assets/js/217dc876.30c8f19d.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[8279],{37216(e,n,i){i.r(n),i.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>h,frontMatter:()=>d,metadata:()=>o,toc:()=>c});var r=i(86070),t=i(81753),a=i(75783);const d={title:"GUID Generation",sidebar_position:3,description:"RCommon's IGuidGenerator abstraction for testable GUID creation \u2014 sequential GUIDs for SQL Server, MySQL, PostgreSQL, and Oracle to reduce index fragmentation."},s="GUID Generation",o={id:"core-concepts/guid-generation",title:"GUID Generation",description:"RCommon's IGuidGenerator abstraction for testable GUID creation \u2014 sequential GUIDs for SQL Server, MySQL, PostgreSQL, and Oracle to reduce index fragmentation.",source:"@site/docs/core-concepts/guid-generation.mdx",sourceDirName:"core-concepts",slug:"/core-concepts/guid-generation",permalink:"/docs/next/core-concepts/guid-generation",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/core-concepts/guid-generation.mdx",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"GUID Generation",sidebar_position:3,description:"RCommon's IGuidGenerator abstraction for testable GUID creation \u2014 sequential GUIDs for SQL Server, MySQL, PostgreSQL, and Oracle to reduce index fragmentation."},sidebar:"docsSidebar",previous:{title:"Guards & Validation",permalink:"/docs/next/core-concepts/guards"},next:{title:"System Time Abstraction",permalink:"/docs/next/core-concepts/system-time"}},l={},c=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Simple (random) GUIDs",id:"simple-random-guids",level:3},{value:"Sequential GUIDs",id:"sequential-guids",level:3},{value:"Behavior on repeated calls",id:"behavior-on-repeated-calls",level:3},{value:"Usage",id:"usage",level:2},{value:"Generating a specific sequential GUID type at runtime",id:"generating-a-specific-sequential-guid-type-at-runtime",level:3},{value:"In unit tests",id:"in-unit-tests",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"IGuidGenerator",id:"iguidgenerator",level:3},{value:"SimpleGuidGenerator",id:"simpleguidgenerator",level:3},{value:"SequentialGuidGenerator",id:"sequentialguidgenerator",level:3},{value:"SequentialGuidGeneratorOptions",id:"sequentialguidgeneratoroptions",level:3},{value:"SequentialGuidType enum",id:"sequentialguidtype-enum",level:3}];function u(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"guid-generation",children:"GUID Generation"}),"\n",(0,r.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,r.jsxs)(n.p,{children:["RCommon abstracts GUID generation behind an ",(0,r.jsx)(n.code,{children:"IGuidGenerator"})," interface for two reasons:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Testability."})," Code that calls ",(0,r.jsx)(n.code,{children:"Guid.NewGuid()"})," directly cannot be controlled in unit tests. Injecting ",(0,r.jsx)(n.code,{children:"IGuidGenerator"})," lets tests substitute a predictable implementation."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Database performance."})," Standard random GUIDs cause index fragmentation in clustered indexes because their values are not monotonically increasing. Sequential GUIDs embed a timestamp in the byte layout so new rows are always appended at the end of the index."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["RCommon ships two implementations out of the box: ",(0,r.jsx)(n.code,{children:"SimpleGuidGenerator"})," and ",(0,r.jsx)(n.code,{children:"SequentialGuidGenerator"}),". You pick one during application configuration; see ",(0,r.jsx)(n.a,{href:"#behavior-on-repeated-calls",children:"Behavior on repeated calls"})," below for what happens when more than one module configures GUID generation."]}),"\n",(0,r.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(a.A,{packageName:"RCommon.Core"}),"\n",(0,r.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsxs)(n.p,{children:["Configure GUID generation inside the ",(0,r.jsx)(n.code,{children:"AddRCommon()"})," builder chain."]}),"\n",(0,r.jsx)(n.h3,{id:"simple-random-guids",children:"Simple (random) GUIDs"}),"\n",(0,r.jsxs)(n.p,{children:["Uses ",(0,r.jsx)(n.code,{children:"Guid.NewGuid()"})," internally. Suitable for non-database use cases or when the underlying database handles GUID ordering itself (e.g., ",(0,r.jsx)(n.code,{children:"NEWSEQUENTIALID()"})," in SQL Server)."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithSimpleGuidGenerator();\n"})}),"\n",(0,r.jsx)(n.h3,{id:"sequential-guids",children:"Sequential GUIDs"}),"\n",(0,r.jsxs)(n.p,{children:["Generates GUIDs with an embedded timestamp so that successive calls produce values that sort in the correct order for a given database platform. The byte layout varies by platform; choose the appropriate ",(0,r.jsx)(n.code,{children:"SequentialGuidType"})," for your database:"]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:(0,r.jsx)(n.code,{children:"SequentialGuidType"})}),(0,r.jsx)(n.th,{children:"Database"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SequentialAsString"})}),(0,r.jsx)(n.td,{children:"MySQL, PostgreSQL"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SequentialAsBinary"})}),(0,r.jsx)(n.td,{children:"Oracle"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SequentialAtEnd"})}),(0,r.jsx)(n.td,{children:"SQL Server (default)"})]})]})]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithSequentialGuidGenerator(options =>\n {\n options.DefaultSequentialGuidType = SequentialGuidType.SequentialAtEnd;\n });\n"})}),"\n",(0,r.jsxs)(n.p,{children:["When ",(0,r.jsx)(n.code,{children:"DefaultSequentialGuidType"})," is left ",(0,r.jsx)(n.code,{children:"null"}),", the generator defaults to ",(0,r.jsx)(n.code,{children:"SequentialAtEnd"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"behavior-on-repeated-calls",children:"Behavior on repeated calls"}),"\n",(0,r.jsxs)(n.p,{children:["GUID generator verbs (",(0,r.jsx)(n.code,{children:"WithSimpleGuidGenerator"}),", ",(0,r.jsx)(n.code,{children:"WithSequentialGuidGenerator"}),") now follow singleton-style modular composition: calling the same generator twice is an idempotent no-op, while calling a different generator after one is already configured throws ",(0,r.jsx)(n.code,{children:"RCommonBuilderException"}),". The exception message names both impl types and offers a remediation hint. See ",(0,r.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]}),"\n",(0,r.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,r.jsxs)(n.p,{children:["Inject ",(0,r.jsx)(n.code,{children:"IGuidGenerator"})," wherever you need to create a new identifier:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\n\npublic class OrderService\n{\n private readonly IGuidGenerator _guidGenerator;\n\n public OrderService(IGuidGenerator guidGenerator)\n {\n _guidGenerator = guidGenerator;\n }\n\n public Order CreateOrder(CustomerId customerId)\n {\n var orderId = _guidGenerator.Create();\n return new Order(orderId, customerId);\n }\n}\n"})}),"\n",(0,r.jsx)(n.h3,{id:"generating-a-specific-sequential-guid-type-at-runtime",children:"Generating a specific sequential GUID type at runtime"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"SequentialGuidGenerator"})," exposes an overload that accepts a ",(0,r.jsx)(n.code,{children:"SequentialGuidType"})," directly, which is useful when a single application works with multiple databases:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"// Only available when you have a reference to SequentialGuidGenerator directly\nvar generator = serviceProvider.GetRequiredService() as SequentialGuidGenerator;\nvar guid = generator?.Create(SequentialGuidType.SequentialAsString);\n"})}),"\n",(0,r.jsxs)(n.p,{children:["For most scenarios, rely on the default configured via ",(0,r.jsx)(n.code,{children:"WithSequentialGuidGenerator"})," and inject ",(0,r.jsx)(n.code,{children:"IGuidGenerator"})," \u2014 the concrete type is an implementation detail."]}),"\n",(0,r.jsx)(n.h3,{id:"in-unit-tests",children:"In unit tests"}),"\n",(0,r.jsxs)(n.p,{children:["Substitute ",(0,r.jsx)(n.code,{children:"IGuidGenerator"})," with a test double that returns a known value:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'public class FakeGuidGenerator : IGuidGenerator\n{\n private readonly Guid _value;\n\n public FakeGuidGenerator(Guid value)\n {\n _value = value;\n }\n\n public Guid Create() => _value;\n}\n\n// In a test\nvar knownId = Guid.Parse("00000000-0000-0000-0000-000000000001");\nvar service = new OrderService(new FakeGuidGenerator(knownId));\nvar order = service.CreateOrder(customerId);\nAssert.Equal(knownId, order.Id);\n'})}),"\n",(0,r.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,r.jsx)(n.h3,{id:"iguidgenerator",children:(0,r.jsx)(n.code,{children:"IGuidGenerator"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Member"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsx)(n.tbody,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"Guid Create()"})}),(0,r.jsxs)(n.td,{children:["Creates and returns a new ",(0,r.jsx)(n.code,{children:"Guid"}),"."]})]})})]}),"\n",(0,r.jsx)(n.h3,{id:"simpleguidgenerator",children:(0,r.jsx)(n.code,{children:"SimpleGuidGenerator"})}),"\n",(0,r.jsxs)(n.p,{children:["Registered by ",(0,r.jsx)(n.code,{children:"WithSimpleGuidGenerator()"}),". Calls ",(0,r.jsx)(n.code,{children:"Guid.NewGuid()"})," on every invocation. Scoped lifetime."]}),"\n",(0,r.jsx)(n.h3,{id:"sequentialguidgenerator",children:(0,r.jsx)(n.code,{children:"SequentialGuidGenerator"})}),"\n",(0,r.jsxs)(n.p,{children:["Registered by ",(0,r.jsx)(n.code,{children:"WithSequentialGuidGenerator(...)"}),". Transient lifetime. Generates a GUID composed of 10 bytes of cryptographically random data and a 6-byte millisecond-resolution timestamp. Byte layout is controlled by ",(0,r.jsx)(n.code,{children:"SequentialGuidType"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"sequentialguidgeneratoroptions",children:(0,r.jsx)(n.code,{children:"SequentialGuidGeneratorOptions"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Property"}),(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Default"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsx)(n.tbody,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"DefaultSequentialGuidType"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SequentialGuidType?"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"null"})," (resolves to ",(0,r.jsx)(n.code,{children:"SequentialAtEnd"}),")"]}),(0,r.jsxs)(n.td,{children:["The byte layout strategy used when ",(0,r.jsx)(n.code,{children:"Create()"})," is called without an explicit type."]})]})})]}),"\n",(0,r.jsxs)(n.h3,{id:"sequentialguidtype-enum",children:[(0,r.jsx)(n.code,{children:"SequentialGuidType"})," enum"]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Value"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SequentialAsString"})}),(0,r.jsx)(n.td,{children:"Timestamp at start; sequential when formatted as a string. Use with MySQL, PostgreSQL."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SequentialAsBinary"})}),(0,r.jsx)(n.td,{children:"Timestamp at start; sequential when compared as a byte array. Use with Oracle."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SequentialAtEnd"})}),(0,r.jsx)(n.td,{children:"Timestamp at end of the Data4 block. Use with SQL Server."})]})]})]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(u,{...e})}):u(e)}},75783(e,n,i){i.d(n,{A:()=>c});var r=i(30758);const t="container_xjrG",a="label_Y4p8",d="commandRow_FY5I",s="command_m7Qs",o="copyButton_u1GK";var l=i(86070);function c({packageName:e,version:n}){const[i,c]=(0,r.useState)(!1),u=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,l.jsxs)("div",{className:t,children:[(0,l.jsx)("div",{className:a,children:"NuGet Package"}),(0,l.jsxs)("div",{className:d,children:[(0,l.jsx)("code",{className:s,children:u}),(0,l.jsx)("button",{className:o,onClick:()=>{navigator.clipboard.writeText(u),c(!0),setTimeout(()=>c(!1),2e3)},title:"Copy to clipboard",children:i?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,i){i.d(n,{R:()=>d,x:()=>s});var r=i(30758);const t={},a=r.createContext(t);function d(e){const n=r.useContext(a);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:d(e.components),r.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/2b9a25fb.319c9caf.js b/website/build/assets/js/2b9a25fb.319c9caf.js new file mode 100644 index 00000000..eb922c27 --- /dev/null +++ b/website/build/assets/js/2b9a25fb.319c9caf.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[7682],{83397(e,n,i){i.r(n),i.d(n,{assets:()=>o,contentTitle:()=>s,default:()=>u,frontMatter:()=>a,metadata:()=>c,toc:()=>l});var t=i(86070),d=i(81753),r=i(75783);const a={title:"MediatR",sidebar_position:5,description:"Route RCommon events through MediatR's notification pipeline using MediatREventHandlingBuilder, with ISubscriber handlers unaware of MediatR internals."},s="MediatR",c={id:"event-handling/mediatr",title:"MediatR",description:"Route RCommon events through MediatR's notification pipeline using MediatREventHandlingBuilder, with ISubscriber handlers unaware of MediatR internals.",source:"@site/docs/event-handling/mediatr.mdx",sourceDirName:"event-handling",slug:"/event-handling/mediatr",permalink:"/docs/next/event-handling/mediatr",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/event-handling/mediatr.mdx",tags:[],version:"current",sidebarPosition:5,frontMatter:{title:"MediatR",sidebar_position:5,description:"Route RCommon events through MediatR's notification pipeline using MediatREventHandlingBuilder, with ISubscriber handlers unaware of MediatR internals."},sidebar:"docsSidebar",previous:{title:"Transactional Outbox",permalink:"/docs/next/event-handling/transactional-outbox"},next:{title:"MassTransit",permalink:"/docs/next/event-handling/masstransit"}},o={},l=[{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Custom MediatR service configuration",id:"custom-mediatr-service-configuration",level:3},{value:"Defining an event",id:"defining-an-event",level:2},{value:"Implementing a subscriber",id:"implementing-a-subscriber",level:2},{value:"Publishing events",id:"publishing-events",level:2},{value:"How MediatR is wired",id:"how-mediatr-is-wired",level:2},{value:"API summary",id:"api-summary",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,d.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"mediatr",children:"MediatR"}),"\n",(0,t.jsxs)(n.p,{children:["RCommon integrates with MediatR to route events through the MediatR notification pipeline. Each event is wrapped in a ",(0,t.jsx)(n.code,{children:"MediatRNotification"})," and published to all registered ",(0,t.jsx)(n.code,{children:"INotificationHandler"})," instances. From the application's perspective, handlers still implement ",(0,t.jsx)(n.code,{children:"ISubscriber"})," \u2014 the MediatR plumbing is hidden behind the abstraction."]}),"\n",(0,t.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,t.jsx)(r.A,{packageName:"RCommon.MediatR"}),"\n",(0,t.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsxs)(n.p,{children:["Use ",(0,t.jsx)(n.code,{children:"WithEventHandling"})," on the RCommon builder. Register a producer and one subscriber per event type:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\nusing RCommon.MediatR;\nusing RCommon.MediatR.Producers;\n\nbuilder.Services.AddRCommon()\n .WithEventHandling(eventHandling =>\n {\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n eventHandling.AddSubscriber();\n });\n"})}),"\n",(0,t.jsx)(n.admonition,{title:"Modular composition",type:"tip",children:(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"WithEventHandling"})," is cache-aware. When multiple modules call it, the cached ",(0,t.jsx)(n.code,{children:"MediatREventHandlingBuilder"})," is reused and each configuration delegate runs against the same instance \u2014 subscriber and producer registrations from every module accumulate. ",(0,t.jsx)(n.code,{children:"AddProducer"})," deduplicates by concrete producer type: registering ",(0,t.jsx)(n.code,{children:"PublishWithMediatREventProducer"})," from both ",(0,t.jsx)(n.code,{children:"OrderingModule"})," and ",(0,t.jsx)(n.code,{children:"NotificationsModule"})," results in a single descriptor. See ",(0,t.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"AddSubscriber"})," does three things internally:"]}),"\n",(0,t.jsxs)(n.ol,{children:["\n",(0,t.jsxs)(n.li,{children:["Registers ",(0,t.jsx)(n.code,{children:"ISubscriber"})," with the DI container."]}),"\n",(0,t.jsxs)(n.li,{children:["Registers a ",(0,t.jsx)(n.code,{children:"MediatREventHandler>"})," as an ",(0,t.jsx)(n.code,{children:"INotificationHandler"})," so MediatR can dispatch the notification."]}),"\n",(0,t.jsxs)(n.li,{children:["Records the event-to-producer association in the ",(0,t.jsx)(n.code,{children:"EventSubscriptionManager"})," so the router routes this event only to the MediatR producer."]}),"\n"]}),"\n",(0,t.jsx)(n.h3,{id:"custom-mediatr-service-configuration",children:"Custom MediatR service configuration"}),"\n",(0,t.jsx)(n.p,{children:"If you need to control which assemblies MediatR scans for handlers, use the three-parameter overload:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithEventHandling(\n eventHandling =>\n {\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n },\n mediatR =>\n {\n mediatR.RegisterServicesFromAssembly(typeof(Program).Assembly);\n });\n"})}),"\n",(0,t.jsx)(n.h2,{id:"defining-an-event",children:"Defining an event"}),"\n",(0,t.jsxs)(n.p,{children:["Events must implement ",(0,t.jsx)(n.code,{children:"ISerializableEvent"}),". The ",(0,t.jsx)(n.code,{children:"ISyncEvent"})," and ",(0,t.jsx)(n.code,{children:"IAsyncEvent"})," markers control dispatch ordering through the ",(0,t.jsx)(n.code,{children:"IEventRouter"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Models.Events;\n\npublic class OrderPlaced : ISyncEvent\n{\n public OrderPlaced(Guid orderId, DateTime placedAt)\n {\n OrderId = orderId;\n PlacedAt = placedAt;\n }\n\n public OrderPlaced() { }\n\n public Guid OrderId { get; }\n public DateTime PlacedAt { get; }\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"implementing-a-subscriber",children:"Implementing a subscriber"}),"\n",(0,t.jsxs)(n.p,{children:["Implement ",(0,t.jsx)(n.code,{children:"ISubscriber"}),". MediatR delivery is transparent:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'using RCommon.EventHandling.Subscribers;\n\npublic class OrderPlacedHandler : ISubscriber\n{\n private readonly ILogger _logger;\n\n public OrderPlacedHandler(ILogger logger)\n {\n _logger = logger;\n }\n\n public async Task HandleAsync(OrderPlaced @event, CancellationToken cancellationToken = default)\n {\n _logger.LogInformation("Order {OrderId} placed at {PlacedAt}", @event.OrderId, @event.PlacedAt);\n await Task.CompletedTask;\n }\n}\n'})}),"\n",(0,t.jsx)(n.h2,{id:"publishing-events",children:"Publishing events"}),"\n",(0,t.jsxs)(n.p,{children:["Produce events through ",(0,t.jsx)(n.code,{children:"IEventRouter"})," or ",(0,t.jsx)(n.code,{children:"IEventProducer"}),". The producer wraps each event in a ",(0,t.jsx)(n.code,{children:"MediatRNotification"})," and calls ",(0,t.jsx)(n.code,{children:"IMediatorService.Publish"}),", which dispatches to all registered notification handlers:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"public class OrderService\n{\n private readonly IEventRouter _eventRouter;\n\n public OrderService(IEventRouter eventRouter)\n {\n _eventRouter = eventRouter;\n }\n\n public async Task PlaceOrderAsync(Order order, CancellationToken cancellationToken)\n {\n // ... persist the order ...\n\n _eventRouter.AddTransactionalEvent(new OrderPlaced(order.Id, DateTime.UtcNow));\n await _eventRouter.RouteEventsAsync(cancellationToken);\n }\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"how-mediatr-is-wired",children:"How MediatR is wired"}),"\n",(0,t.jsxs)(n.p,{children:["Internally, ",(0,t.jsx)(n.code,{children:"PublishWithMediatREventProducer"})," calls ",(0,t.jsx)(n.code,{children:"IMediatorService.Publish(@event, cancellationToken)"}),". The mediator service wraps the event in a ",(0,t.jsx)(n.code,{children:"MediatRNotification"})," and dispatches it through MediatR's notification pipeline. ",(0,t.jsx)(n.code,{children:"MediatREventHandler"})," receives the notification and resolves ",(0,t.jsx)(n.code,{children:"ISubscriber"})," from the DI container to call ",(0,t.jsx)(n.code,{children:"HandleAsync"}),"."]}),"\n",(0,t.jsx)(n.p,{children:"This means MediatR pipeline behaviours (logging, validation, unit-of-work) apply to event handlers just as they apply to command and query handlers, giving you a consistent cross-cutting-concerns story."}),"\n",(0,t.jsx)(n.h2,{id:"api-summary",children:"API summary"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Type"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"MediatREventHandlingBuilder"})}),(0,t.jsxs)(n.td,{children:["Builder used with ",(0,t.jsx)(n.code,{children:"WithEventHandling"})," to configure MediatR event handling"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"IMediatREventHandlingBuilder"})}),(0,t.jsx)(n.td,{children:"Interface for the MediatR event handling builder"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"PublishWithMediatREventProducer"})}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"IEventProducer"})," that publishes via ",(0,t.jsx)(n.code,{children:"IMediatorService.Publish"})," (fan-out)"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"SendWithMediatREventProducer"})}),(0,t.jsxs)(n.td,{children:[(0,t.jsx)(n.code,{children:"IEventProducer"})," that sends via ",(0,t.jsx)(n.code,{children:"IMediatorService.Send"})," (point-to-point)"]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"MediatREventHandler"})}),(0,t.jsxs)(n.td,{children:["Internal ",(0,t.jsx)(n.code,{children:"INotificationHandler"})," that bridges MediatR to ",(0,t.jsx)(n.code,{children:"ISubscriber"})]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"MediatRNotification"})}),(0,t.jsxs)(n.td,{children:["MediatR ",(0,t.jsx)(n.code,{children:"INotification"})," wrapper used to carry events through the pipeline"]})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,d.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},75783(e,n,i){i.d(n,{A:()=>l});var t=i(30758);const d="container_xjrG",r="label_Y4p8",a="commandRow_FY5I",s="command_m7Qs",c="copyButton_u1GK";var o=i(86070);function l({packageName:e,version:n}){const[i,l]=(0,t.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,o.jsxs)("div",{className:d,children:[(0,o.jsx)("div",{className:r,children:"NuGet Package"}),(0,o.jsxs)("div",{className:a,children:[(0,o.jsx)("code",{className:s,children:h}),(0,o.jsx)("button",{className:c,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:i?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,i){i.d(n,{R:()=>a,x:()=>s});var t=i(30758);const d={},r=t.createContext(d);function a(e){const n=t.useContext(r);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(d):e.components||d:a(e.components),t.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/2b9a25fb.99463b22.js b/website/build/assets/js/2b9a25fb.99463b22.js deleted file mode 100644 index c17ad72e..00000000 --- a/website/build/assets/js/2b9a25fb.99463b22.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[7682],{83397(e,n,i){i.r(n),i.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>u,frontMatter:()=>a,metadata:()=>c,toc:()=>o});var r=i(86070),t=i(81753),d=i(75783);const a={title:"MediatR",sidebar_position:5,description:"Route RCommon events through MediatR's notification pipeline using MediatREventHandlingBuilder, with ISubscriber handlers unaware of MediatR internals."},s="MediatR",c={id:"event-handling/mediatr",title:"MediatR",description:"Route RCommon events through MediatR's notification pipeline using MediatREventHandlingBuilder, with ISubscriber handlers unaware of MediatR internals.",source:"@site/docs/event-handling/mediatr.mdx",sourceDirName:"event-handling",slug:"/event-handling/mediatr",permalink:"/docs/next/event-handling/mediatr",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/event-handling/mediatr.mdx",tags:[],version:"current",sidebarPosition:5,frontMatter:{title:"MediatR",sidebar_position:5,description:"Route RCommon events through MediatR's notification pipeline using MediatREventHandlingBuilder, with ISubscriber handlers unaware of MediatR internals."},sidebar:"docsSidebar",previous:{title:"Transactional Outbox",permalink:"/docs/next/event-handling/transactional-outbox"},next:{title:"MassTransit",permalink:"/docs/next/event-handling/masstransit"}},l={},o=[{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Custom MediatR service configuration",id:"custom-mediatr-service-configuration",level:3},{value:"Defining an event",id:"defining-an-event",level:2},{value:"Implementing a subscriber",id:"implementing-a-subscriber",level:2},{value:"Publishing events",id:"publishing-events",level:2},{value:"How MediatR is wired",id:"how-mediatr-is-wired",level:2},{value:"API summary",id:"api-summary",level:2}];function h(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"mediatr",children:"MediatR"}),"\n",(0,r.jsxs)(n.p,{children:["RCommon integrates with MediatR to route events through the MediatR notification pipeline. Each event is wrapped in a ",(0,r.jsx)(n.code,{children:"MediatRNotification"})," and published to all registered ",(0,r.jsx)(n.code,{children:"INotificationHandler"})," instances. From the application's perspective, handlers still implement ",(0,r.jsx)(n.code,{children:"ISubscriber"})," \u2014 the MediatR plumbing is hidden behind the abstraction."]}),"\n",(0,r.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(d.A,{packageName:"RCommon.MediatR"}),"\n",(0,r.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.code,{children:"WithEventHandling"})," on the RCommon builder. Register a producer and one subscriber per event type:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\r\nusing RCommon.MediatR;\r\nusing RCommon.MediatR.Producers;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithEventHandling(eventHandling =>\r\n {\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n eventHandling.AddSubscriber();\r\n });\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"AddSubscriber"})," does three things internally:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["Registers ",(0,r.jsx)(n.code,{children:"ISubscriber"})," with the DI container."]}),"\n",(0,r.jsxs)(n.li,{children:["Registers a ",(0,r.jsx)(n.code,{children:"MediatREventHandler>"})," as an ",(0,r.jsx)(n.code,{children:"INotificationHandler"})," so MediatR can dispatch the notification."]}),"\n",(0,r.jsxs)(n.li,{children:["Records the event-to-producer association in the ",(0,r.jsx)(n.code,{children:"EventSubscriptionManager"})," so the router routes this event only to the MediatR producer."]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"custom-mediatr-service-configuration",children:"Custom MediatR service configuration"}),"\n",(0,r.jsx)(n.p,{children:"If you need to control which assemblies MediatR scans for handlers, use the three-parameter overload:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithEventHandling(\r\n eventHandling =>\r\n {\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n },\r\n mediatR =>\r\n {\r\n mediatR.RegisterServicesFromAssembly(typeof(Program).Assembly);\r\n });\n"})}),"\n",(0,r.jsx)(n.h2,{id:"defining-an-event",children:"Defining an event"}),"\n",(0,r.jsxs)(n.p,{children:["Events must implement ",(0,r.jsx)(n.code,{children:"ISerializableEvent"}),". The ",(0,r.jsx)(n.code,{children:"ISyncEvent"})," and ",(0,r.jsx)(n.code,{children:"IAsyncEvent"})," markers control dispatch ordering through the ",(0,r.jsx)(n.code,{children:"IEventRouter"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Models.Events;\r\n\r\npublic class OrderPlaced : ISyncEvent\r\n{\r\n public OrderPlaced(Guid orderId, DateTime placedAt)\r\n {\r\n OrderId = orderId;\r\n PlacedAt = placedAt;\r\n }\r\n\r\n public OrderPlaced() { }\r\n\r\n public Guid OrderId { get; }\r\n public DateTime PlacedAt { get; }\r\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"implementing-a-subscriber",children:"Implementing a subscriber"}),"\n",(0,r.jsxs)(n.p,{children:["Implement ",(0,r.jsx)(n.code,{children:"ISubscriber"}),". MediatR delivery is transparent:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon.EventHandling.Subscribers;\r\n\r\npublic class OrderPlacedHandler : ISubscriber\r\n{\r\n private readonly ILogger _logger;\r\n\r\n public OrderPlacedHandler(ILogger logger)\r\n {\r\n _logger = logger;\r\n }\r\n\r\n public async Task HandleAsync(OrderPlaced @event, CancellationToken cancellationToken = default)\r\n {\r\n _logger.LogInformation("Order {OrderId} placed at {PlacedAt}", @event.OrderId, @event.PlacedAt);\r\n await Task.CompletedTask;\r\n }\r\n}\n'})}),"\n",(0,r.jsx)(n.h2,{id:"publishing-events",children:"Publishing events"}),"\n",(0,r.jsxs)(n.p,{children:["Produce events through ",(0,r.jsx)(n.code,{children:"IEventRouter"})," or ",(0,r.jsx)(n.code,{children:"IEventProducer"}),". The producer wraps each event in a ",(0,r.jsx)(n.code,{children:"MediatRNotification"})," and calls ",(0,r.jsx)(n.code,{children:"IMediatorService.Publish"}),", which dispatches to all registered notification handlers:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"public class OrderService\r\n{\r\n private readonly IEventRouter _eventRouter;\r\n\r\n public OrderService(IEventRouter eventRouter)\r\n {\r\n _eventRouter = eventRouter;\r\n }\r\n\r\n public async Task PlaceOrderAsync(Order order, CancellationToken cancellationToken)\r\n {\r\n // ... persist the order ...\r\n\r\n _eventRouter.AddTransactionalEvent(new OrderPlaced(order.Id, DateTime.UtcNow));\r\n await _eventRouter.RouteEventsAsync(cancellationToken);\r\n }\r\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"how-mediatr-is-wired",children:"How MediatR is wired"}),"\n",(0,r.jsxs)(n.p,{children:["Internally, ",(0,r.jsx)(n.code,{children:"PublishWithMediatREventProducer"})," calls ",(0,r.jsx)(n.code,{children:"IMediatorService.Publish(@event, cancellationToken)"}),". The mediator service wraps the event in a ",(0,r.jsx)(n.code,{children:"MediatRNotification"})," and dispatches it through MediatR's notification pipeline. ",(0,r.jsx)(n.code,{children:"MediatREventHandler"})," receives the notification and resolves ",(0,r.jsx)(n.code,{children:"ISubscriber"})," from the DI container to call ",(0,r.jsx)(n.code,{children:"HandleAsync"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"This means MediatR pipeline behaviours (logging, validation, unit-of-work) apply to event handlers just as they apply to command and query handlers, giving you a consistent cross-cutting-concerns story."}),"\n",(0,r.jsx)(n.h2,{id:"api-summary",children:"API summary"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"MediatREventHandlingBuilder"})}),(0,r.jsxs)(n.td,{children:["Builder used with ",(0,r.jsx)(n.code,{children:"WithEventHandling"})," to configure MediatR event handling"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"IMediatREventHandlingBuilder"})}),(0,r.jsx)(n.td,{children:"Interface for the MediatR event handling builder"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"PublishWithMediatREventProducer"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"IEventProducer"})," that publishes via ",(0,r.jsx)(n.code,{children:"IMediatorService.Publish"})," (fan-out)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SendWithMediatREventProducer"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"IEventProducer"})," that sends via ",(0,r.jsx)(n.code,{children:"IMediatorService.Send"})," (point-to-point)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"MediatREventHandler"})}),(0,r.jsxs)(n.td,{children:["Internal ",(0,r.jsx)(n.code,{children:"INotificationHandler"})," that bridges MediatR to ",(0,r.jsx)(n.code,{children:"ISubscriber"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"MediatRNotification"})}),(0,r.jsxs)(n.td,{children:["MediatR ",(0,r.jsx)(n.code,{children:"INotification"})," wrapper used to carry events through the pipeline"]})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},75783(e,n,i){i.d(n,{A:()=>o});var r=i(30758);const t="container_xjrG",d="label_Y4p8",a="commandRow_FY5I",s="command_m7Qs",c="copyButton_u1GK";var l=i(86070);function o({packageName:e,version:n}){const[i,o]=(0,r.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,l.jsxs)("div",{className:t,children:[(0,l.jsx)("div",{className:d,children:"NuGet Package"}),(0,l.jsxs)("div",{className:a,children:[(0,l.jsx)("code",{className:s,children:h}),(0,l.jsx)("button",{className:c,onClick:()=>{navigator.clipboard.writeText(h),o(!0),setTimeout(()=>o(!1),2e3)},title:"Copy to clipboard",children:i?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,i){i.d(n,{R:()=>a,x:()=>s});var r=i(30758);const t={},d=r.createContext(t);function a(e){const n=r.useContext(d);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:a(e.components),r.createElement(d.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/2c2268a5.a125bcd5.js b/website/build/assets/js/2c2268a5.a125bcd5.js deleted file mode 100644 index fb70b98d..00000000 --- a/website/build/assets/js/2c2268a5.a125bcd5.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[7486],{24631(e,r,n){n.r(r),n.d(r,{assets:()=>d,contentTitle:()=>o,default:()=>u,frontMatter:()=>c,metadata:()=>a,toc:()=>l});var i=n(86070),t=n(81753),s=n(75783);const c={title:"Web Utilities",sidebar_position:2,description:"RCommon.Web bridges ASP.NET Core's HttpContext.User to ICurrentPrincipalAccessor, replacing the thread-based default for correct identity resolution in web apps."},o="Web Utilities",a={id:"security-web/web-utilities",title:"Web Utilities",description:"RCommon.Web bridges ASP.NET Core's HttpContext.User to ICurrentPrincipalAccessor, replacing the thread-based default for correct identity resolution in web apps.",source:"@site/docs/security-web/web-utilities.mdx",sourceDirName:"security-web",slug:"/security-web/web-utilities",permalink:"/docs/next/security-web/web-utilities",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/security-web/web-utilities.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Web Utilities",sidebar_position:2,description:"RCommon.Web bridges ASP.NET Core's HttpContext.User to ICurrentPrincipalAccessor, replacing the thread-based default for correct identity resolution in web apps."},sidebar:"docsSidebar",previous:{title:"Authorization",permalink:"/docs/next/security-web/authorization"},next:{title:"Architecture Guides",permalink:"/docs/next/category/architecture-guides"}},d={},l=[{value:"Overview",id:"overview",level:2},{value:"Why a separate web package?",id:"why-a-separate-web-package",level:3},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Combined with other RCommon features",id:"combined-with-other-rcommon-features",level:3},{value:"Usage",id:"usage",level:2},{value:"Injecting ICurrentUser in a controller or service",id:"injecting-icurrentuser-in-a-controller-or-service",level:3},{value:"Using ICurrentUser in middleware",id:"using-icurrentuser-in-middleware",level:3},{value:"Temporarily overriding the principal",id:"temporarily-overriding-the-principal",level:3},{value:"Switching from ThreadCurrentPrincipalAccessor",id:"switching-from-threadcurrentprincipalaccessor",level:3},{value:"API Summary",id:"api-summary",level:2}];function h(e){const r={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(r.h1,{id:"web-utilities",children:"Web Utilities"}),"\n",(0,i.jsx)(r.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"RCommon.Web"})," provides ASP.NET Core-specific integrations that bridge RCommon's security abstractions to the HTTP request pipeline. Its primary purpose is to replace the thread-based principal accessor with one that reads the authenticated user from the current ",(0,i.jsx)(r.code,{children:"HttpContext"}),", which is the correct source of identity in ASP.NET Core web applications."]}),"\n",(0,i.jsx)(r.p,{children:"The package contains:"}),"\n",(0,i.jsxs)(r.ul,{children:["\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"})," \u2014 an ",(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})," implementation that reads ",(0,i.jsx)(r.code,{children:"HttpContext.User"})," via ",(0,i.jsx)(r.code,{children:"IHttpContextAccessor"}),"."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"WebConfigurationExtensions"})," \u2014 a startup extension method that registers all security services using the HTTP-context-based accessor."]}),"\n"]}),"\n",(0,i.jsx)(r.h3,{id:"why-a-separate-web-package",children:"Why a separate web package?"}),"\n",(0,i.jsxs)(r.p,{children:["RCommon's core security library (",(0,i.jsx)(r.code,{children:"RCommon.Security"}),") has no ASP.NET Core dependency. Its default principal accessor reads from ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"}),", which works in console apps, background services, and test harnesses. In an ASP.NET Core web app the authenticated user is set on ",(0,i.jsx)(r.code,{children:"HttpContext.User"})," by the authentication middleware, not on ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"}),". ",(0,i.jsx)(r.code,{children:"RCommon.Web"})," provides the adapter that makes the correct source available to all RCommon services."]}),"\n",(0,i.jsx)(r.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(s.A,{packageName:"RCommon.Web"}),"\n",(0,i.jsxs)(r.p,{children:["This package depends on ",(0,i.jsx)(r.code,{children:"RCommon.Security"}),", which is pulled in automatically."]}),"\n",(0,i.jsx)(r.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(r.p,{children:["Call ",(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," instead of ",(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessor()"})," in your ASP.NET Core startup:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"using RCommon;\r\n\r\nvar builder = WebApplication.CreateBuilder(args);\r\n\r\nbuilder.Services.AddRCommon(config =>\r\n{\r\n config.WithClaimsAndPrincipalAccessorForWeb();\r\n});\r\n\r\nvar app = builder.Build();\r\n\r\n// Ensure authentication and authorization middleware run before your endpoints.\r\napp.UseAuthentication();\r\napp.UseAuthorization();\r\n\r\napp.MapControllers();\r\napp.Run();\n"})}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," registers the following services:"]}),"\n",(0,i.jsxs)(r.table,{children:[(0,i.jsx)(r.thead,{children:(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.th,{children:"Interface"}),(0,i.jsx)(r.th,{children:"Implementation"})]})}),(0,i.jsxs)(r.tbody,{children:[(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"})})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentUser"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentUser"})})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentClient"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentClient"})})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ITenantIdAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ClaimsTenantIdAccessor"})})]})]})]}),"\n",(0,i.jsxs)(r.p,{children:["It also calls ",(0,i.jsx)(r.code,{children:"services.AddHttpContextAccessor()"})," so that ",(0,i.jsx)(r.code,{children:"IHttpContextAccessor"})," is available for injection."]}),"\n",(0,i.jsx)(r.h3,{id:"combined-with-other-rcommon-features",children:"Combined with other RCommon features"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," is fluent and chains naturally with persistence, multi-tenancy, and other configuration:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'builder.Services.AddRCommon(config =>\r\n{\r\n config\r\n .WithClaimsAndPrincipalAccessorForWeb()\r\n .WithPersistence(ef =>\r\n {\r\n ef.AddDbContext(\r\n "App",\r\n options => options.UseSqlServer(\r\n builder.Configuration.GetConnectionString("Default")));\r\n })\r\n .WithMultiTenancy>(mt => { });\r\n});\n'})}),"\n",(0,i.jsx)(r.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(r.h3,{id:"injecting-icurrentuser-in-a-controller-or-service",children:"Injecting ICurrentUser in a controller or service"}),"\n",(0,i.jsxs)(r.p,{children:["Once ",(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," is registered, inject ",(0,i.jsx)(r.code,{children:"ICurrentUser"})," anywhere in your application. The resolved identity comes from the authenticated ",(0,i.jsx)(r.code,{children:"HttpContext.User"})," for the current request:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Users;\r\nusing Microsoft.AspNetCore.Authorization;\r\nusing Microsoft.AspNetCore.Mvc;\r\n\r\n[Authorize]\r\n[ApiController]\r\n[Route("api/[controller]")]\r\npublic class ProfileController : ControllerBase\r\n{\r\n private readonly ICurrentUser _currentUser;\r\n\r\n public ProfileController(ICurrentUser currentUser)\r\n {\r\n _currentUser = currentUser;\r\n }\r\n\r\n [HttpGet]\r\n public IActionResult GetProfile()\r\n {\r\n return Ok(new\r\n {\r\n UserId = _currentUser.Id,\r\n Roles = _currentUser.Roles,\r\n TenantId = _currentUser.TenantId\r\n });\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"using-icurrentuser-in-middleware",children:"Using ICurrentUser in middleware"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Users;\r\n\r\npublic class RequestAuditMiddleware\r\n{\r\n private readonly RequestDelegate _next;\r\n\r\n public RequestAuditMiddleware(RequestDelegate next)\r\n {\r\n _next = next;\r\n }\r\n\r\n public async Task InvokeAsync(HttpContext context, ICurrentUser currentUser)\r\n {\r\n if (currentUser.IsAuthenticated)\r\n {\r\n var userId = currentUser.Id?.ToString() ?? "anonymous";\r\n context.Items["AuditUserId"] = userId;\r\n }\r\n\r\n await _next(context);\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"temporarily-overriding-the-principal",children:"Temporarily overriding the principal"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"})," inherits the ",(0,i.jsx)(r.code,{children:"Change()"})," method from ",(0,i.jsx)(r.code,{children:"CurrentPrincipalAccessorBase"}),". The override is stored in ",(0,i.jsx)(r.code,{children:"AsyncLocal"})," and does not mutate ",(0,i.jsx)(r.code,{children:"HttpContext.User"}),", making it safe to use in middleware or background tasks that need to act as a different identity:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Claims;\r\nusing System.Security.Claims;\r\n\r\npublic class IntegrationEventHandler\r\n{\r\n private readonly ICurrentPrincipalAccessor _principalAccessor;\r\n private readonly ICurrentUser _currentUser;\r\n\r\n public IntegrationEventHandler(\r\n ICurrentPrincipalAccessor principalAccessor,\r\n ICurrentUser currentUser)\r\n {\r\n _principalAccessor = principalAccessor;\r\n _currentUser = currentUser;\r\n }\r\n\r\n public async Task HandleAsync(IntegrationEvent @event)\r\n {\r\n // Run as a system identity when processing out-of-band events.\r\n var systemIdentity = new ClaimsIdentity(new[]\r\n {\r\n new Claim(ClaimTypesConst.UserId, @event.InitiatingUserId.ToString()),\r\n new Claim(ClaimTypesConst.TenantId, @event.TenantId)\r\n }, "System");\r\n\r\n using (_principalAccessor.Change(new ClaimsPrincipal(systemIdentity)))\r\n {\r\n // ICurrentUser now resolves against systemIdentity.\r\n Console.WriteLine($"Processing as user {_currentUser.Id} for tenant {_currentUser.TenantId}");\r\n await ProcessEventAsync(@event);\r\n }\r\n // Original principal (or null outside a request) is restored here.\r\n }\r\n\r\n private Task ProcessEventAsync(IntegrationEvent @event) => Task.CompletedTask;\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"switching-from-threadcurrentprincipalaccessor",children:"Switching from ThreadCurrentPrincipalAccessor"}),"\n",(0,i.jsxs)(r.p,{children:["If you started with ",(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessor()"})," and are migrating to an ASP.NET Core host, replace it with ",(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"}),". The registered interfaces are identical; only the ",(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})," implementation changes:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"// Before (non-web or background service):\r\nconfig.WithClaimsAndPrincipalAccessor();\r\n\r\n// After (ASP.NET Core web application):\r\nconfig.WithClaimsAndPrincipalAccessorForWeb();\n"})}),"\n",(0,i.jsxs)(r.p,{children:["No other code changes are needed. All services that depend on ",(0,i.jsx)(r.code,{children:"ICurrentUser"}),", ",(0,i.jsx)(r.code,{children:"ICurrentClient"}),", or ",(0,i.jsx)(r.code,{children:"ITenantIdAccessor"})," continue to work without modification."]}),"\n",(0,i.jsx)(r.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(r.table,{children:[(0,i.jsx)(r.thead,{children:(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.th,{children:"Type"}),(0,i.jsx)(r.th,{children:"Package"}),(0,i.jsx)(r.th,{children:"Description"})]})}),(0,i.jsxs)(r.tbody,{children:[(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Web"})}),(0,i.jsxs)(r.td,{children:["Reads the current ",(0,i.jsx)(r.code,{children:"ClaimsPrincipal"})," from ",(0,i.jsx)(r.code,{children:"IHttpContextAccessor.HttpContext.User"}),"; falls back to ",(0,i.jsx)(r.code,{children:"null"})," outside of a request"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"WebConfigurationExtensions.WithClaimsAndPrincipalAccessorForWeb"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Web"})}),(0,i.jsxs)(r.td,{children:["Registers ",(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"}),", ",(0,i.jsx)(r.code,{children:"ICurrentUser"}),", ",(0,i.jsx)(r.code,{children:"ICurrentClient"}),", ",(0,i.jsx)(r.code,{children:"ITenantIdAccessor"}),", and ",(0,i.jsx)(r.code,{children:"IHttpContextAccessor"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Core abstraction; exposes ",(0,i.jsx)(r.code,{children:"Principal"})," and ",(0,i.jsx)(r.code,{children:"Change(ClaimsPrincipal)"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentPrincipalAccessorBase"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Abstract base; ",(0,i.jsx)(r.code,{children:"Change()"})," stores the override in ",(0,i.jsx)(r.code,{children:"AsyncLocal"})," so it flows across async continuations"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ThreadCurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Non-web default; reads ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"})]})]})]})]})]})}function u(e={}){const{wrapper:r}={...(0,t.R)(),...e.components};return r?(0,i.jsx)(r,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},75783(e,r,n){n.d(r,{A:()=>l});var i=n(30758);const t="container_xjrG",s="label_Y4p8",c="commandRow_FY5I",o="command_m7Qs",a="copyButton_u1GK";var d=n(86070);function l({packageName:e,version:r}){const[n,l]=(0,i.useState)(!1),h=r?`dotnet add package ${e} --version ${r}`:`dotnet add package ${e}`;return(0,d.jsxs)("div",{className:t,children:[(0,d.jsx)("div",{className:s,children:"NuGet Package"}),(0,d.jsxs)("div",{className:c,children:[(0,d.jsx)("code",{className:o,children:h}),(0,d.jsx)("button",{className:a,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:n?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,r,n){n.d(r,{R:()=>c,x:()=>o});var i=n(30758);const t={},s=i.createContext(t);function c(e){const r=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(r):{...r,...e}},[r,e])}function o(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:c(e.components),i.createElement(s.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/2c2268a5.a41f1a69.js b/website/build/assets/js/2c2268a5.a41f1a69.js new file mode 100644 index 00000000..688c675d --- /dev/null +++ b/website/build/assets/js/2c2268a5.a41f1a69.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[7486],{24631(e,n,r){r.r(n),r.d(n,{assets:()=>a,contentTitle:()=>o,default:()=>u,frontMatter:()=>c,metadata:()=>d,toc:()=>l});var i=r(86070),t=r(81753),s=r(75783);const c={title:"Web Utilities",sidebar_position:2,description:"RCommon.Web bridges ASP.NET Core's HttpContext.User to ICurrentPrincipalAccessor, replacing the thread-based default for correct identity resolution in web apps."},o="Web Utilities",d={id:"security-web/web-utilities",title:"Web Utilities",description:"RCommon.Web bridges ASP.NET Core's HttpContext.User to ICurrentPrincipalAccessor, replacing the thread-based default for correct identity resolution in web apps.",source:"@site/docs/security-web/web-utilities.mdx",sourceDirName:"security-web",slug:"/security-web/web-utilities",permalink:"/docs/next/security-web/web-utilities",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/security-web/web-utilities.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Web Utilities",sidebar_position:2,description:"RCommon.Web bridges ASP.NET Core's HttpContext.User to ICurrentPrincipalAccessor, replacing the thread-based default for correct identity resolution in web apps."},sidebar:"docsSidebar",previous:{title:"Authorization",permalink:"/docs/next/security-web/authorization"},next:{title:"Architecture Guides",permalink:"/docs/next/category/architecture-guides"}},a={},l=[{value:"Overview",id:"overview",level:2},{value:"Why a separate web package?",id:"why-a-separate-web-package",level:3},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Combined with other RCommon features",id:"combined-with-other-rcommon-features",level:3},{value:"Usage",id:"usage",level:2},{value:"Injecting ICurrentUser in a controller or service",id:"injecting-icurrentuser-in-a-controller-or-service",level:3},{value:"Using ICurrentUser in middleware",id:"using-icurrentuser-in-middleware",level:3},{value:"Temporarily overriding the principal",id:"temporarily-overriding-the-principal",level:3},{value:"Switching from ThreadCurrentPrincipalAccessor",id:"switching-from-threadcurrentprincipalaccessor",level:3},{value:"API Summary",id:"api-summary",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"web-utilities",children:"Web Utilities"}),"\n",(0,i.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"RCommon.Web"})," provides ASP.NET Core-specific integrations that bridge RCommon's security abstractions to the HTTP request pipeline. Its primary purpose is to replace the thread-based principal accessor with one that reads the authenticated user from the current ",(0,i.jsx)(n.code,{children:"HttpContext"}),", which is the correct source of identity in ASP.NET Core web applications."]}),"\n",(0,i.jsx)(n.p,{children:"The package contains:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"HttpContextCurrentPrincipalAccessor"})," \u2014 an ",(0,i.jsx)(n.code,{children:"ICurrentPrincipalAccessor"})," implementation that reads ",(0,i.jsx)(n.code,{children:"HttpContext.User"})," via ",(0,i.jsx)(n.code,{children:"IHttpContextAccessor"}),"."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"WebConfigurationExtensions"})," \u2014 a startup extension method that registers all security services using the HTTP-context-based accessor."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"why-a-separate-web-package",children:"Why a separate web package?"}),"\n",(0,i.jsxs)(n.p,{children:["RCommon's core security library (",(0,i.jsx)(n.code,{children:"RCommon.Security"}),") has no ASP.NET Core dependency. Its default principal accessor reads from ",(0,i.jsx)(n.code,{children:"Thread.CurrentPrincipal"}),", which works in console apps, background services, and test harnesses. In an ASP.NET Core web app the authenticated user is set on ",(0,i.jsx)(n.code,{children:"HttpContext.User"})," by the authentication middleware, not on ",(0,i.jsx)(n.code,{children:"Thread.CurrentPrincipal"}),". ",(0,i.jsx)(n.code,{children:"RCommon.Web"})," provides the adapter that makes the correct source available to all RCommon services."]}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(s.A,{packageName:"RCommon.Web"}),"\n",(0,i.jsxs)(n.p,{children:["This package depends on ",(0,i.jsx)(n.code,{children:"RCommon.Security"}),", which is pulled in automatically."]}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["Call ",(0,i.jsx)(n.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," instead of ",(0,i.jsx)(n.code,{children:"WithClaimsAndPrincipalAccessor()"})," in your ASP.NET Core startup:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddRCommon(config =>\n{\n config.WithClaimsAndPrincipalAccessorForWeb();\n});\n\nvar app = builder.Build();\n\n// Ensure authentication and authorization middleware run before your endpoints.\napp.UseAuthentication();\napp.UseAuthorization();\n\napp.MapControllers();\napp.Run();\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," registers the following services:"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Interface"}),(0,i.jsx)(n.th,{children:"Implementation"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ICurrentPrincipalAccessor"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"HttpContextCurrentPrincipalAccessor"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ICurrentUser"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"CurrentUser"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ICurrentClient"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"CurrentClient"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ITenantIdAccessor"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ClaimsTenantIdAccessor"})})]})]})]}),"\n",(0,i.jsxs)(n.p,{children:["It also calls ",(0,i.jsx)(n.code,{children:"services.AddHttpContextAccessor()"})," so that ",(0,i.jsx)(n.code,{children:"IHttpContextAccessor"})," is available for injection."]}),"\n",(0,i.jsx)(n.admonition,{title:"Modular composition",type:"tip",children:(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," is ",(0,i.jsx)(n.code,{children:"TryAdd"}),"-hardened \u2014 repeat calls from multiple modules are idempotent. Each principal/claims/tenant interface is registered exactly once. Modules that need access to ",(0,i.jsx)(n.code,{children:"ICurrentUser"})," can each call this verb without coordinating with other modules. Do not mix ",(0,i.jsx)(n.code,{children:"WithClaimsAndPrincipalAccessor()"})," and ",(0,i.jsx)(n.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," in the same process \u2014 ",(0,i.jsx)(n.code,{children:"TryAdd"})," will keep whichever ran first and silently ignore the other. See ",(0,i.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,i.jsx)(n.h3,{id:"combined-with-other-rcommon-features",children:"Combined with other RCommon features"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," is fluent and chains naturally with persistence, multi-tenancy, and other configuration:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'builder.Services.AddRCommon(config =>\n{\n config\n .WithClaimsAndPrincipalAccessorForWeb()\n .WithPersistence(ef =>\n {\n ef.AddDbContext(\n "App",\n options => options.UseSqlServer(\n builder.Configuration.GetConnectionString("Default")));\n })\n .WithMultiTenancy>(mt => { });\n});\n'})}),"\n",(0,i.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(n.h3,{id:"injecting-icurrentuser-in-a-controller-or-service",children:"Injecting ICurrentUser in a controller or service"}),"\n",(0,i.jsxs)(n.p,{children:["Once ",(0,i.jsx)(n.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," is registered, inject ",(0,i.jsx)(n.code,{children:"ICurrentUser"})," anywhere in your application. The resolved identity comes from the authenticated ",(0,i.jsx)(n.code,{children:"HttpContext.User"})," for the current request:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'using RCommon.Security.Users;\nusing Microsoft.AspNetCore.Authorization;\nusing Microsoft.AspNetCore.Mvc;\n\n[Authorize]\n[ApiController]\n[Route("api/[controller]")]\npublic class ProfileController : ControllerBase\n{\n private readonly ICurrentUser _currentUser;\n\n public ProfileController(ICurrentUser currentUser)\n {\n _currentUser = currentUser;\n }\n\n [HttpGet]\n public IActionResult GetProfile()\n {\n return Ok(new\n {\n UserId = _currentUser.Id,\n Roles = _currentUser.Roles,\n TenantId = _currentUser.TenantId\n });\n }\n}\n'})}),"\n",(0,i.jsx)(n.h3,{id:"using-icurrentuser-in-middleware",children:"Using ICurrentUser in middleware"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'using RCommon.Security.Users;\n\npublic class RequestAuditMiddleware\n{\n private readonly RequestDelegate _next;\n\n public RequestAuditMiddleware(RequestDelegate next)\n {\n _next = next;\n }\n\n public async Task InvokeAsync(HttpContext context, ICurrentUser currentUser)\n {\n if (currentUser.IsAuthenticated)\n {\n var userId = currentUser.Id ?? "anonymous";\n context.Items["AuditUserId"] = userId;\n }\n\n await _next(context);\n }\n}\n'})}),"\n",(0,i.jsx)(n.h3,{id:"temporarily-overriding-the-principal",children:"Temporarily overriding the principal"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"HttpContextCurrentPrincipalAccessor"})," inherits the ",(0,i.jsx)(n.code,{children:"Change()"})," method from ",(0,i.jsx)(n.code,{children:"CurrentPrincipalAccessorBase"}),". The override is stored in ",(0,i.jsx)(n.code,{children:"AsyncLocal"})," and does not mutate ",(0,i.jsx)(n.code,{children:"HttpContext.User"}),", making it safe to use in middleware or background tasks that need to act as a different identity:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'using RCommon.Security.Claims;\nusing System.Security.Claims;\n\npublic class IntegrationEventHandler\n{\n private readonly ICurrentPrincipalAccessor _principalAccessor;\n private readonly ICurrentUser _currentUser;\n\n public IntegrationEventHandler(\n ICurrentPrincipalAccessor principalAccessor,\n ICurrentUser currentUser)\n {\n _principalAccessor = principalAccessor;\n _currentUser = currentUser;\n }\n\n public async Task HandleAsync(IntegrationEvent @event)\n {\n // Run as a system identity when processing out-of-band events.\n var systemIdentity = new ClaimsIdentity(new[]\n {\n new Claim(ClaimTypesConst.UserId, @event.InitiatingUserId.ToString()),\n new Claim(ClaimTypesConst.TenantId, @event.TenantId)\n }, "System");\n\n using (_principalAccessor.Change(new ClaimsPrincipal(systemIdentity)))\n {\n // ICurrentUser now resolves against systemIdentity.\n Console.WriteLine($"Processing as user {_currentUser.Id} for tenant {_currentUser.TenantId}");\n await ProcessEventAsync(@event);\n }\n // Original principal (or null outside a request) is restored here.\n }\n\n private Task ProcessEventAsync(IntegrationEvent @event) => Task.CompletedTask;\n}\n'})}),"\n",(0,i.jsx)(n.h3,{id:"switching-from-threadcurrentprincipalaccessor",children:"Switching from ThreadCurrentPrincipalAccessor"}),"\n",(0,i.jsxs)(n.p,{children:["If you started with ",(0,i.jsx)(n.code,{children:"WithClaimsAndPrincipalAccessor()"})," and are migrating to an ASP.NET Core host, replace it with ",(0,i.jsx)(n.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"}),". The registered interfaces are identical; only the ",(0,i.jsx)(n.code,{children:"ICurrentPrincipalAccessor"})," implementation changes:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"// Before (non-web or background service):\nconfig.WithClaimsAndPrincipalAccessor();\n\n// After (ASP.NET Core web application):\nconfig.WithClaimsAndPrincipalAccessorForWeb();\n"})}),"\n",(0,i.jsxs)(n.p,{children:["No other code changes are needed. All services that depend on ",(0,i.jsx)(n.code,{children:"ICurrentUser"}),", ",(0,i.jsx)(n.code,{children:"ICurrentClient"}),", or ",(0,i.jsx)(n.code,{children:"ITenantIdAccessor"})," continue to work without modification."]}),"\n",(0,i.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Package"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"HttpContextCurrentPrincipalAccessor"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Web"})}),(0,i.jsxs)(n.td,{children:["Reads the current ",(0,i.jsx)(n.code,{children:"ClaimsPrincipal"})," from ",(0,i.jsx)(n.code,{children:"IHttpContextAccessor.HttpContext.User"}),"; falls back to ",(0,i.jsx)(n.code,{children:"null"})," outside of a request"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"WebConfigurationExtensions.WithClaimsAndPrincipalAccessorForWeb"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Web"})}),(0,i.jsxs)(n.td,{children:["Registers ",(0,i.jsx)(n.code,{children:"HttpContextCurrentPrincipalAccessor"}),", ",(0,i.jsx)(n.code,{children:"ICurrentUser"}),", ",(0,i.jsx)(n.code,{children:"ICurrentClient"}),", ",(0,i.jsx)(n.code,{children:"ITenantIdAccessor"}),", and ",(0,i.jsx)(n.code,{children:"IHttpContextAccessor"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ICurrentPrincipalAccessor"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Security"})}),(0,i.jsxs)(n.td,{children:["Core abstraction; exposes ",(0,i.jsx)(n.code,{children:"Principal"})," and ",(0,i.jsx)(n.code,{children:"Change(ClaimsPrincipal)"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"CurrentPrincipalAccessorBase"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Security"})}),(0,i.jsxs)(n.td,{children:["Abstract base; ",(0,i.jsx)(n.code,{children:"Change()"})," stores the override in ",(0,i.jsx)(n.code,{children:"AsyncLocal"})," so it flows across async continuations"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ThreadCurrentPrincipalAccessor"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Security"})}),(0,i.jsxs)(n.td,{children:["Non-web default; reads ",(0,i.jsx)(n.code,{children:"Thread.CurrentPrincipal"})]})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},75783(e,n,r){r.d(n,{A:()=>l});var i=r(30758);const t="container_xjrG",s="label_Y4p8",c="commandRow_FY5I",o="command_m7Qs",d="copyButton_u1GK";var a=r(86070);function l({packageName:e,version:n}){const[r,l]=(0,i.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,a.jsxs)("div",{className:t,children:[(0,a.jsx)("div",{className:s,children:"NuGet Package"}),(0,a.jsxs)("div",{className:c,children:[(0,a.jsx)("code",{className:o,children:h}),(0,a.jsx)("button",{className:d,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:r?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,r){r.d(n,{R:()=>c,x:()=>o});var i=r(30758);const t={},s=i.createContext(t);function c(e){const n=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:c(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/2fb564d3.a788fc33.js b/website/build/assets/js/2fb564d3.a788fc33.js deleted file mode 100644 index 19f7cc7e..00000000 --- a/website/build/assets/js/2fb564d3.a788fc33.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[1909],{14667(e,n,r){r.r(n),r.d(n,{assets:()=>c,contentTitle:()=>l,default:()=>v,frontMatter:()=>s,metadata:()=>o,toc:()=>a});var i=r(86070),d=r(81753),t=r(75783);const s={title:"Wolverine",sidebar_position:5,description:"Integrate WolverineFx as a distributed event producer in RCommon, supporting fan-out publish and point-to-point send via ISubscriber without Wolverine-specific handler code."},l="Wolverine",o={id:"cqrs-mediator/wolverine",title:"Wolverine",description:"Integrate WolverineFx as a distributed event producer in RCommon, supporting fan-out publish and point-to-point send via ISubscriber without Wolverine-specific handler code.",source:"@site/docs/cqrs-mediator/wolverine.mdx",sourceDirName:"cqrs-mediator",slug:"/cqrs-mediator/wolverine",permalink:"/docs/next/cqrs-mediator/wolverine",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/cqrs-mediator/wolverine.mdx",tags:[],version:"current",sidebarPosition:5,frontMatter:{title:"Wolverine",sidebar_position:5,description:"Integrate WolverineFx as a distributed event producer in RCommon, supporting fan-out publish and point-to-point send via ISubscriber without Wolverine-specific handler code."},sidebar:"docsSidebar",previous:{title:"MediatR",permalink:"/docs/next/cqrs-mediator/mediatr"},next:{title:"Event Handling",permalink:"/docs/next/category/event-handling"}},c={},a=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Usage",id:"usage",level:2},{value:"Defining an event",id:"defining-an-event",level:3},{value:"Creating a subscriber",id:"creating-a-subscriber",level:3},{value:"Registration",id:"registration",level:3},{value:"Producing events",id:"producing-events",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"IWolverineEventHandlingBuilder extension methods",id:"iwolverineeventhandlingbuilder-extension-methods",level:3},{value:"PublishWithWolverineEventProducer",id:"publishwithwolverineeventproducer",level:3},{value:"SendWithWolverineEventProducer",id:"sendwithwolverineeventproducer",level:3},{value:"WolverineEventHandler<TEvent>",id:"wolverineeventhandlertevent",level:3},{value:"Key interfaces and base types",id:"key-interfaces-and-base-types",level:3}];function h(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"wolverine",children:"Wolverine"}),"\n",(0,i.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"RCommon.Wolverine"})," integrates ",(0,i.jsx)(n.a,{href:"https://wolverinefx.net",children:"WolverineFx"})," as a distributed event handler for the RCommon event handling pipeline. It is focused on event production and subscription rather than the request-response mediator pattern."]}),"\n",(0,i.jsx)(n.p,{children:"The package provides:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})," / ",(0,i.jsx)(n.code,{children:"IWolverineEventHandlingBuilder"})," \u2014 registers Wolverine-based subscribers through the RCommon fluent builder."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})," \u2014 publishes events to all Wolverine handlers using ",(0,i.jsx)(n.code,{children:"IMessageBus.PublishAsync"})," (fan-out)."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"SendWithWolverineEventProducer"})," \u2014 sends events to a single Wolverine handler using ",(0,i.jsx)(n.code,{children:"IMessageBus.SendAsync"})," (point-to-point)."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"WolverineEventHandler"})," \u2014 bridges Wolverine's message dispatch to RCommon's ",(0,i.jsx)(n.code,{children:"ISubscriber"})," abstraction."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Events must implement ",(0,i.jsx)(n.code,{children:"ISerializableEvent"})," (from ",(0,i.jsx)(n.code,{children:"RCommon.Models"}),") to flow through the Wolverine producers."]}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(t.A,{packageName:"RCommon.Wolverine"}),"\n",(0,i.jsxs)(n.p,{children:["This package depends on ",(0,i.jsx)(n.code,{children:"RCommon.Core"})," and ",(0,i.jsx)(n.code,{children:"WolverineFx"}),". Wolverine itself requires host-level setup via ",(0,i.jsx)(n.code,{children:"UseWolverine()"})," on the application host builder \u2014 see the Wolverine documentation for transport and endpoint configuration."]}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsx)(n.p,{children:"Wolverine is configured at the host level and within the RCommon builder. Register the Wolverine host extension on the application host builder, then wire up RCommon subscribers."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\r\nusing RCommon.Wolverine;\r\nusing Wolverine;\r\n\r\nvar builder = WebApplication.CreateBuilder(args);\r\n\r\n// Configure Wolverine at the host level\r\nbuilder.Host.UseWolverine(opts =>\r\n{\r\n // Configure transports, endpoints, and discovery here\r\n opts.Discovery.IncludeAssembly(typeof(Program).Assembly);\r\n});\r\n\r\n// Configure RCommon with Wolverine event handling\r\nbuilder.Services.AddRCommon()\r\n .WithEventHandling(wolverine =>\r\n {\r\n wolverine.AddSubscriber();\r\n wolverine.AddSubscriber();\r\n });\n"})}),"\n",(0,i.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(n.h3,{id:"defining-an-event",children:"Defining an event"}),"\n",(0,i.jsxs)(n.p,{children:["Events dispatched through Wolverine must implement ",(0,i.jsx)(n.code,{children:"ISerializableEvent"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Models.Events;\r\n\r\npublic class OrderPlacedEvent : ISerializableEvent\r\n{\r\n public OrderPlacedEvent(Guid orderId, Guid customerId, DateTime placedAt)\r\n {\r\n OrderId = orderId;\r\n CustomerId = customerId;\r\n PlacedAt = placedAt;\r\n }\r\n\r\n public Guid OrderId { get; }\r\n public Guid CustomerId { get; }\r\n public DateTime PlacedAt { get; }\r\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"creating-a-subscriber",children:"Creating a subscriber"}),"\n",(0,i.jsxs)(n.p,{children:["Implement ",(0,i.jsx)(n.code,{children:"ISubscriber"})," from ",(0,i.jsx)(n.code,{children:"RCommon.EventHandling.Subscribers"}),". This is the same interface used by MediatR subscribers \u2014 the Wolverine adapter bridges message dispatch to it automatically through ",(0,i.jsx)(n.code,{children:"WolverineEventHandler"}),"."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon.EventHandling.Subscribers;\r\n\r\npublic class OrderPlacedEventHandler : ISubscriber\r\n{\r\n private readonly IEmailService _emailService;\r\n\r\n public OrderPlacedEventHandler(IEmailService emailService)\r\n {\r\n _emailService = emailService;\r\n }\r\n\r\n public async Task HandleAsync(OrderPlacedEvent @event, CancellationToken cancellationToken = default)\r\n {\r\n await _emailService.SendOrderConfirmationAsync(@event.CustomerId, @event.OrderId, cancellationToken);\r\n }\r\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"registration",children:"Registration"}),"\n",(0,i.jsxs)(n.p,{children:["Register subscribers using the ",(0,i.jsx)(n.code,{children:"IWolverineEventHandlingBuilder"})," extension method. Each call records the event-to-producer subscription in the ",(0,i.jsx)(n.code,{children:"EventSubscriptionManager"})," so the router only delivers the event to the producers registered for it."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithEventHandling(wolverine =>\r\n {\r\n wolverine.AddSubscriber();\r\n });\n"})}),"\n",(0,i.jsx)(n.p,{children:"If you need to construct the subscriber with a factory delegate:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"wolverine.AddSubscriber(\r\n sp => new OrderPlacedEventHandler(sp.GetRequiredService()));\n"})}),"\n",(0,i.jsx)(n.h3,{id:"producing-events",children:"Producing events"}),"\n",(0,i.jsxs)(n.p,{children:["Use ",(0,i.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})," for fan-out delivery (all subscribers receive the event) or ",(0,i.jsx)(n.code,{children:"SendWithWolverineEventProducer"})," for point-to-point delivery (one handler). Register the appropriate producer when setting up your event bus:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon.EventHandling.Producers;\r\nusing RCommon.Wolverine.Producers;\r\n\r\n// Fan-out: all subscribed handlers receive the event\r\nbuilder.Services.AddScoped();\r\n\r\n// Point-to-point: only one handler receives the event\r\nbuilder.Services.AddScoped();\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Produce events through RCommon's ",(0,i.jsx)(n.code,{children:"IEventBus"})," or by raising domain events that flow through the event router \u2014 the producers are invoked by the routing infrastructure automatically."]}),"\n",(0,i.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(n.h3,{id:"iwolverineeventhandlingbuilder-extension-methods",children:[(0,i.jsx)(n.code,{children:"IWolverineEventHandlingBuilder"})," extension methods"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Method"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"AddSubscriber()"})}),(0,i.jsxs)(n.td,{children:["Registers ",(0,i.jsx)(n.code,{children:"TEventHandler"})," as a scoped ",(0,i.jsx)(n.code,{children:"ISubscriber"})," and records the subscription in the ",(0,i.jsx)(n.code,{children:"EventSubscriptionManager"}),"."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"AddSubscriber(Func)"})}),(0,i.jsx)(n.td,{children:"Same as above, but uses a factory delegate to construct the subscriber."})]})]})]}),"\n",(0,i.jsx)(n.h3,{id:"publishwithwolverineeventproducer",children:(0,i.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Member"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ProduceEventAsync(T, CancellationToken)"})}),(0,i.jsxs)(n.td,{children:["Calls ",(0,i.jsx)(n.code,{children:"IMessageBus.PublishAsync"})," to deliver the event to all Wolverine handlers subscribed to ",(0,i.jsx)(n.code,{children:"T"}),". Skips delivery if the event type is not subscribed to this producer."]})]})})]}),"\n",(0,i.jsx)(n.h3,{id:"sendwithwolverineeventproducer",children:(0,i.jsx)(n.code,{children:"SendWithWolverineEventProducer"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Member"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ProduceEventAsync(T, CancellationToken)"})}),(0,i.jsxs)(n.td,{children:["Calls ",(0,i.jsx)(n.code,{children:"IMessageBus.SendAsync"})," to deliver the event to a single Wolverine handler. Skips delivery if the event type is not subscribed to this producer."]})]})})]}),"\n",(0,i.jsx)(n.h3,{id:"wolverineeventhandlertevent",children:(0,i.jsx)(n.code,{children:"WolverineEventHandler"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Member"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"HandleAsync(TEvent, CancellationToken)"})}),(0,i.jsxs)(n.td,{children:["Called by Wolverine's message dispatch infrastructure. Delegates to the injected ",(0,i.jsx)(n.code,{children:"ISubscriber"}),"."]})]})})]}),"\n",(0,i.jsx)(n.h3,{id:"key-interfaces-and-base-types",children:"Key interfaces and base types"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Package"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IWolverineEventHandlingBuilder"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Wolverine"})}),(0,i.jsxs)(n.td,{children:["Builder interface for configuring Wolverine event handling. Extends ",(0,i.jsx)(n.code,{children:"IEventHandlingBuilder"}),"."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Wolverine"})}),(0,i.jsxs)(n.td,{children:["Default implementation. Constructed by ",(0,i.jsx)(n.code,{children:"WithEventHandling"}),"."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ISerializableEvent"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Models"})}),(0,i.jsx)(n.td,{children:"Marker interface required on all events dispatched through Wolverine producers."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ISubscriber"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,i.jsx)(n.td,{children:"Application-level handler interface. Implement this for your event handling logic."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IWolverineEventHandler"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Wolverine"})}),(0,i.jsxs)(n.td,{children:["Wolverine-facing handler interface implemented by ",(0,i.jsx)(n.code,{children:"WolverineEventHandler"}),"."]})]})]})]})]})}function v(e={}){const{wrapper:n}={...(0,d.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},75783(e,n,r){r.d(n,{A:()=>a});var i=r(30758);const d="container_xjrG",t="label_Y4p8",s="commandRow_FY5I",l="command_m7Qs",o="copyButton_u1GK";var c=r(86070);function a({packageName:e,version:n}){const[r,a]=(0,i.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,c.jsxs)("div",{className:d,children:[(0,c.jsx)("div",{className:t,children:"NuGet Package"}),(0,c.jsxs)("div",{className:s,children:[(0,c.jsx)("code",{className:l,children:h}),(0,c.jsx)("button",{className:o,onClick:()=>{navigator.clipboard.writeText(h),a(!0),setTimeout(()=>a(!1),2e3)},title:"Copy to clipboard",children:r?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,r){r.d(n,{R:()=>s,x:()=>l});var i=r(30758);const d={},t=i.createContext(d);function s(e){const n=i.useContext(t);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(d):e.components||d:s(e.components),i.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/2fb564d3.fdcbd40a.js b/website/build/assets/js/2fb564d3.fdcbd40a.js new file mode 100644 index 00000000..35cec922 --- /dev/null +++ b/website/build/assets/js/2fb564d3.fdcbd40a.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[1909],{14667(e,n,r){r.r(n),r.d(n,{assets:()=>c,contentTitle:()=>l,default:()=>v,frontMatter:()=>s,metadata:()=>o,toc:()=>a});var i=r(86070),d=r(81753),t=r(75783);const s={title:"Wolverine",sidebar_position:5,description:"Integrate WolverineFx as a distributed event producer in RCommon, supporting fan-out publish and point-to-point send via ISubscriber without Wolverine-specific handler code."},l="Wolverine",o={id:"cqrs-mediator/wolverine",title:"Wolverine",description:"Integrate WolverineFx as a distributed event producer in RCommon, supporting fan-out publish and point-to-point send via ISubscriber without Wolverine-specific handler code.",source:"@site/docs/cqrs-mediator/wolverine.mdx",sourceDirName:"cqrs-mediator",slug:"/cqrs-mediator/wolverine",permalink:"/docs/next/cqrs-mediator/wolverine",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/cqrs-mediator/wolverine.mdx",tags:[],version:"current",sidebarPosition:5,frontMatter:{title:"Wolverine",sidebar_position:5,description:"Integrate WolverineFx as a distributed event producer in RCommon, supporting fan-out publish and point-to-point send via ISubscriber without Wolverine-specific handler code."},sidebar:"docsSidebar",previous:{title:"MediatR",permalink:"/docs/next/cqrs-mediator/mediatr"},next:{title:"Event Handling",permalink:"/docs/next/category/event-handling"}},c={},a=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Usage",id:"usage",level:2},{value:"Defining an event",id:"defining-an-event",level:3},{value:"Creating a subscriber",id:"creating-a-subscriber",level:3},{value:"Registration",id:"registration",level:3},{value:"Producing events",id:"producing-events",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"IWolverineEventHandlingBuilder extension methods",id:"iwolverineeventhandlingbuilder-extension-methods",level:3},{value:"PublishWithWolverineEventProducer",id:"publishwithwolverineeventproducer",level:3},{value:"SendWithWolverineEventProducer",id:"sendwithwolverineeventproducer",level:3},{value:"WolverineEventHandler<TEvent>",id:"wolverineeventhandlertevent",level:3},{value:"Key interfaces and base types",id:"key-interfaces-and-base-types",level:3}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"wolverine",children:"Wolverine"}),"\n",(0,i.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"RCommon.Wolverine"})," integrates ",(0,i.jsx)(n.a,{href:"https://wolverinefx.net",children:"WolverineFx"})," as a distributed event handler for the RCommon event handling pipeline. It is focused on event production and subscription rather than the request-response mediator pattern."]}),"\n",(0,i.jsx)(n.p,{children:"The package provides:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})," / ",(0,i.jsx)(n.code,{children:"IWolverineEventHandlingBuilder"})," \u2014 registers Wolverine-based subscribers through the RCommon fluent builder."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})," \u2014 publishes events to all Wolverine handlers using ",(0,i.jsx)(n.code,{children:"IMessageBus.PublishAsync"})," (fan-out)."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"SendWithWolverineEventProducer"})," \u2014 sends events to a single Wolverine handler using ",(0,i.jsx)(n.code,{children:"IMessageBus.SendAsync"})," (point-to-point)."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"WolverineEventHandler"})," \u2014 bridges Wolverine's message dispatch to RCommon's ",(0,i.jsx)(n.code,{children:"ISubscriber"})," abstraction."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["Events must implement ",(0,i.jsx)(n.code,{children:"ISerializableEvent"})," (from ",(0,i.jsx)(n.code,{children:"RCommon.Models"}),") to flow through the Wolverine producers."]}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(t.A,{packageName:"RCommon.Wolverine"}),"\n",(0,i.jsxs)(n.p,{children:["This package depends on ",(0,i.jsx)(n.code,{children:"RCommon.Core"})," and ",(0,i.jsx)(n.code,{children:"WolverineFx"}),". Wolverine itself requires host-level setup via ",(0,i.jsx)(n.code,{children:"UseWolverine()"})," on the application host builder \u2014 see the Wolverine documentation for transport and endpoint configuration."]}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsx)(n.p,{children:"Wolverine is configured at the host level and within the RCommon builder. Register the Wolverine host extension on the application host builder, then wire up RCommon subscribers."}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\nusing RCommon.Wolverine;\nusing Wolverine;\n\nvar builder = WebApplication.CreateBuilder(args);\n\n// Configure Wolverine at the host level\nbuilder.Host.UseWolverine(opts =>\n{\n // Configure transports, endpoints, and discovery here\n opts.Discovery.IncludeAssembly(typeof(Program).Assembly);\n});\n\n// Configure RCommon with Wolverine event handling\nbuilder.Services.AddRCommon()\n .WithEventHandling(wolverine =>\n {\n wolverine.AddSubscriber();\n wolverine.AddSubscriber();\n });\n"})}),"\n",(0,i.jsx)(n.admonition,{title:"Modular composition",type:"tip",children:(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"WithEventHandling"})," is a cache-aware sub-builder verb. When multiple modules call it, the cached ",(0,i.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})," is reused and each module's ",(0,i.jsx)(n.code,{children:"AddSubscriber"})," and ",(0,i.jsx)(n.code,{children:"AddProducer"})," registrations accumulate on the same instance. ",(0,i.jsx)(n.code,{children:"AddProducer"})," deduplicates by concrete producer type, so the same producer registered from two modules yields exactly one descriptor. Note that the host-level ",(0,i.jsx)(n.code,{children:"builder.Host.UseWolverine(...)"})," call must happen exactly once in the application host; only the RCommon sub-builder participates in module-level composition. See ",(0,i.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,i.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(n.h3,{id:"defining-an-event",children:"Defining an event"}),"\n",(0,i.jsxs)(n.p,{children:["Events dispatched through Wolverine must implement ",(0,i.jsx)(n.code,{children:"ISerializableEvent"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Models.Events;\n\npublic class OrderPlacedEvent : ISerializableEvent\n{\n public OrderPlacedEvent(Guid orderId, Guid customerId, DateTime placedAt)\n {\n OrderId = orderId;\n CustomerId = customerId;\n PlacedAt = placedAt;\n }\n\n public Guid OrderId { get; }\n public Guid CustomerId { get; }\n public DateTime PlacedAt { get; }\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"creating-a-subscriber",children:"Creating a subscriber"}),"\n",(0,i.jsxs)(n.p,{children:["Implement ",(0,i.jsx)(n.code,{children:"ISubscriber"})," from ",(0,i.jsx)(n.code,{children:"RCommon.EventHandling.Subscribers"}),". This is the same interface used by MediatR subscribers \u2014 the Wolverine adapter bridges message dispatch to it automatically through ",(0,i.jsx)(n.code,{children:"WolverineEventHandler"}),"."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon.EventHandling.Subscribers;\n\npublic class OrderPlacedEventHandler : ISubscriber\n{\n private readonly IEmailService _emailService;\n\n public OrderPlacedEventHandler(IEmailService emailService)\n {\n _emailService = emailService;\n }\n\n public async Task HandleAsync(OrderPlacedEvent @event, CancellationToken cancellationToken = default)\n {\n await _emailService.SendOrderConfirmationAsync(@event.CustomerId, @event.OrderId, cancellationToken);\n }\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"registration",children:"Registration"}),"\n",(0,i.jsxs)(n.p,{children:["Register subscribers using the ",(0,i.jsx)(n.code,{children:"IWolverineEventHandlingBuilder"})," extension method. Each call records the event-to-producer subscription in the ",(0,i.jsx)(n.code,{children:"EventSubscriptionManager"})," so the router only delivers the event to the producers registered for it."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithEventHandling(wolverine =>\n {\n wolverine.AddSubscriber();\n });\n"})}),"\n",(0,i.jsx)(n.p,{children:"If you need to construct the subscriber with a factory delegate:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"wolverine.AddSubscriber(\n sp => new OrderPlacedEventHandler(sp.GetRequiredService()));\n"})}),"\n",(0,i.jsx)(n.h3,{id:"producing-events",children:"Producing events"}),"\n",(0,i.jsxs)(n.p,{children:["Use ",(0,i.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})," for fan-out delivery (all subscribers receive the event) or ",(0,i.jsx)(n.code,{children:"SendWithWolverineEventProducer"})," for point-to-point delivery (one handler). Register the appropriate producer when setting up your event bus:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon.EventHandling.Producers;\nusing RCommon.Wolverine.Producers;\n\n// Fan-out: all subscribed handlers receive the event\nbuilder.Services.AddScoped();\n\n// Point-to-point: only one handler receives the event\nbuilder.Services.AddScoped();\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Produce events through RCommon's ",(0,i.jsx)(n.code,{children:"IEventBus"})," or by raising domain events that flow through the event router \u2014 the producers are invoked by the routing infrastructure automatically."]}),"\n",(0,i.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(n.h3,{id:"iwolverineeventhandlingbuilder-extension-methods",children:[(0,i.jsx)(n.code,{children:"IWolverineEventHandlingBuilder"})," extension methods"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Method"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"AddSubscriber()"})}),(0,i.jsxs)(n.td,{children:["Registers ",(0,i.jsx)(n.code,{children:"TEventHandler"})," as a scoped ",(0,i.jsx)(n.code,{children:"ISubscriber"})," and records the subscription in the ",(0,i.jsx)(n.code,{children:"EventSubscriptionManager"}),"."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"AddSubscriber(Func)"})}),(0,i.jsx)(n.td,{children:"Same as above, but uses a factory delegate to construct the subscriber."})]})]})]}),"\n",(0,i.jsx)(n.h3,{id:"publishwithwolverineeventproducer",children:(0,i.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Member"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ProduceEventAsync(T, CancellationToken)"})}),(0,i.jsxs)(n.td,{children:["Calls ",(0,i.jsx)(n.code,{children:"IMessageBus.PublishAsync"})," to deliver the event to all Wolverine handlers subscribed to ",(0,i.jsx)(n.code,{children:"T"}),". Skips delivery if the event type is not subscribed to this producer."]})]})})]}),"\n",(0,i.jsx)(n.h3,{id:"sendwithwolverineeventproducer",children:(0,i.jsx)(n.code,{children:"SendWithWolverineEventProducer"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Member"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ProduceEventAsync(T, CancellationToken)"})}),(0,i.jsxs)(n.td,{children:["Calls ",(0,i.jsx)(n.code,{children:"IMessageBus.SendAsync"})," to deliver the event to a single Wolverine handler. Skips delivery if the event type is not subscribed to this producer."]})]})})]}),"\n",(0,i.jsx)(n.h3,{id:"wolverineeventhandlertevent",children:(0,i.jsx)(n.code,{children:"WolverineEventHandler"})}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Member"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsx)(n.tbody,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"HandleAsync(TEvent, CancellationToken)"})}),(0,i.jsxs)(n.td,{children:["Called by Wolverine's message dispatch infrastructure. Delegates to the injected ",(0,i.jsx)(n.code,{children:"ISubscriber"}),"."]})]})})]}),"\n",(0,i.jsx)(n.h3,{id:"key-interfaces-and-base-types",children:"Key interfaces and base types"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Package"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IWolverineEventHandlingBuilder"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Wolverine"})}),(0,i.jsxs)(n.td,{children:["Builder interface for configuring Wolverine event handling. Extends ",(0,i.jsx)(n.code,{children:"IEventHandlingBuilder"}),"."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Wolverine"})}),(0,i.jsxs)(n.td,{children:["Default implementation. Constructed by ",(0,i.jsx)(n.code,{children:"WithEventHandling"}),"."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ISerializableEvent"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Models"})}),(0,i.jsx)(n.td,{children:"Marker interface required on all events dispatched through Wolverine producers."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ISubscriber"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,i.jsx)(n.td,{children:"Application-level handler interface. Implement this for your event handling logic."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IWolverineEventHandler"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Wolverine"})}),(0,i.jsxs)(n.td,{children:["Wolverine-facing handler interface implemented by ",(0,i.jsx)(n.code,{children:"WolverineEventHandler"}),"."]})]})]})]})]})}function v(e={}){const{wrapper:n}={...(0,d.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},75783(e,n,r){r.d(n,{A:()=>a});var i=r(30758);const d="container_xjrG",t="label_Y4p8",s="commandRow_FY5I",l="command_m7Qs",o="copyButton_u1GK";var c=r(86070);function a({packageName:e,version:n}){const[r,a]=(0,i.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,c.jsxs)("div",{className:d,children:[(0,c.jsx)("div",{className:t,children:"NuGet Package"}),(0,c.jsxs)("div",{className:s,children:[(0,c.jsx)("code",{className:l,children:h}),(0,c.jsx)("button",{className:o,onClick:()=>{navigator.clipboard.writeText(h),a(!0),setTimeout(()=>a(!1),2e3)},title:"Copy to clipboard",children:r?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,r){r.d(n,{R:()=>s,x:()=>l});var i=r(30758);const d={},t=i.createContext(d);function s(e){const n=i.useContext(t);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(d):e.components||d:s(e.components),i.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/30d27832.08d95ec6.js b/website/build/assets/js/30d27832.08d95ec6.js deleted file mode 100644 index 97bda8c6..00000000 --- a/website/build/assets/js/30d27832.08d95ec6.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[4890],{12397(e,n,i){i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>s,default:()=>h,frontMatter:()=>o,metadata:()=>d,toc:()=>c});var r=i(86070),t=i(81753);const o={title:"Configuration & Bootstrapping",sidebar_position:4,description:"Configure RCommon with AddRCommon() \u2014 fluent builder methods for GUID generation, system time, persistence, unit of work, mediator, and event handling."},s="Configuration & Bootstrapping",d={id:"getting-started/configuration",title:"Configuration & Bootstrapping",description:"Configure RCommon with AddRCommon() \u2014 fluent builder methods for GUID generation, system time, persistence, unit of work, mediator, and event handling.",source:"@site/docs/getting-started/configuration.mdx",sourceDirName:"getting-started",slug:"/getting-started/configuration",permalink:"/docs/next/getting-started/configuration",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/getting-started/configuration.mdx",tags:[],version:"current",sidebarPosition:4,frontMatter:{title:"Configuration & Bootstrapping",sidebar_position:4,description:"Configure RCommon with AddRCommon() \u2014 fluent builder methods for GUID generation, system time, persistence, unit of work, mediator, and event handling."},sidebar:"docsSidebar",previous:{title:"Quick Start Guide",permalink:"/docs/next/getting-started/quick-start"},next:{title:"Dependency Injection",permalink:"/docs/next/getting-started/dependency-injection"}},a={},c=[{value:"The AddRCommon() extension method",id:"the-addrcommon-extension-method",level:2},{value:"Builder chain",id:"builder-chain",level:2},{value:"Core builder methods",id:"core-builder-methods",level:2},{value:"WithSequentialGuidGenerator",id:"withsequentialguidgenerator",level:3},{value:"WithSimpleGuidGenerator",id:"withsimpleguidgenerator",level:3},{value:"WithDateTimeSystem",id:"withdatetimesystem",level:3},{value:"WithCommonFactory",id:"withcommonfactory",level:3},{value:"Persistence configuration",id:"persistence-configuration",level:2},{value:"Unit of work configuration",id:"unit-of-work-configuration",level:2},{value:"Mediator configuration",id:"mediator-configuration",level:2},{value:"Event handling configuration",id:"event-handling-configuration",level:2},{value:"Complete example",id:"complete-example",level:2},{value:"Diagnostics",id:"diagnostics",level:2}];function l(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"configuration--bootstrapping",children:"Configuration & Bootstrapping"}),"\n",(0,r.jsx)(n.p,{children:"RCommon uses a single fluent builder entry point. All configuration happens at application startup before the DI container is built."}),"\n",(0,r.jsx)(n.h2,{id:"the-addrcommon-extension-method",children:"The AddRCommon() extension method"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"AddRCommon()"})," is an extension method on ",(0,r.jsx)(n.code,{children:"IServiceCollection"})," defined in ",(0,r.jsx)(n.code,{children:"RCommon.Core"}),". Calling it creates an ",(0,r.jsx)(n.code,{children:"RCommonBuilder"}),", registers the core framework services, and returns an ",(0,r.jsx)(n.code,{children:"IRCommonBuilder"})," for further configuration."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\r\n\r\n// In Program.cs or a DI registration method\r\nbuilder.Services.AddRCommon();\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Calling ",(0,r.jsx)(n.code,{children:"AddRCommon()"})," always registers these services regardless of what else is configured:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"EventSubscriptionManager"})," (singleton) \u2014 tracks event-to-producer subscriptions"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"IEventBus"})," backed by ",(0,r.jsx)(n.code,{children:"InMemoryEventBus"})," (singleton) \u2014 in-process publish/subscribe"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"IEventRouter"})," backed by ",(0,r.jsx)(n.code,{children:"InMemoryTransactionalEventRouter"})," (scoped) \u2014 coordinates domain events with the unit of work"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"CachingOptions"})," configured with caching disabled by default"]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"builder-chain",children:"Builder chain"}),"\n",(0,r.jsxs)(n.p,{children:["The return value of ",(0,r.jsx)(n.code,{children:"AddRCommon()"})," is ",(0,r.jsx)(n.code,{children:"IRCommonBuilder"}),". Every ",(0,r.jsx)(n.code,{children:"With*"})," method on the builder returns the same ",(0,r.jsx)(n.code,{children:"IRCommonBuilder"})," instance so calls can be chained:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithSequentialGuidGenerator(...)\r\n .WithDateTimeSystem(...)\r\n .WithPersistence(...)\r\n .WithUnitOfWork(...)\r\n .WithMediator(...)\r\n .WithEventHandling(...);\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Every ",(0,r.jsx)(n.code,{children:"With*"})," call is independent and opt-in. Omitting a call means those services are not registered."]}),"\n",(0,r.jsx)(n.h2,{id:"core-builder-methods",children:"Core builder methods"}),"\n",(0,r.jsx)(n.h3,{id:"withsequentialguidgenerator",children:"WithSequentialGuidGenerator"}),"\n",(0,r.jsxs)(n.p,{children:["Registers ",(0,r.jsx)(n.code,{children:"IGuidGenerator"})," as a ",(0,r.jsx)(n.code,{children:"SequentialGuidGenerator"}),". Sequential GUIDs are ordered in a way that reduces index fragmentation in SQL databases."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:".WithSequentialGuidGenerator(options =>\r\n{\r\n options.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString;\r\n})\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"SequentialGuidType"})," values:"]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Value"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SequentialAsString"})}),(0,r.jsx)(n.td,{children:"Sequential in string representation \u2014 compatible with MySQL and PostgreSQL"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SequentialAsBinary"})}),(0,r.jsx)(n.td,{children:"Sequential in binary representation \u2014 compatible with Oracle"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SequentialAtEnd"})}),(0,r.jsx)(n.td,{children:"Sequential at the end \u2014 compatible with SQL Server"})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:["Only one GUID generator may be configured. Calling ",(0,r.jsx)(n.code,{children:"WithSequentialGuidGenerator"})," or ",(0,r.jsx)(n.code,{children:"WithSimpleGuidGenerator"})," a second time throws ",(0,r.jsx)(n.code,{children:"RCommonBuilderException"}),"."]}),"\n",(0,r.jsx)(n.h3,{id:"withsimpleguidgenerator",children:"WithSimpleGuidGenerator"}),"\n",(0,r.jsxs)(n.p,{children:["Registers ",(0,r.jsx)(n.code,{children:"IGuidGenerator"})," as a ",(0,r.jsx)(n.code,{children:"SimpleGuidGenerator"})," that wraps ",(0,r.jsx)(n.code,{children:"Guid.NewGuid()"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:".WithSimpleGuidGenerator()\n"})}),"\n",(0,r.jsx)(n.h3,{id:"withdatetimesystem",children:"WithDateTimeSystem"}),"\n",(0,r.jsxs)(n.p,{children:["Registers ",(0,r.jsx)(n.code,{children:"ISystemTime"})," as ",(0,r.jsx)(n.code,{children:"SystemTime"}),". Inject ",(0,r.jsx)(n.code,{children:"ISystemTime"})," wherever you need the current time so that tests can substitute a known value."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:".WithDateTimeSystem(options =>\r\n{\r\n options.Kind = DateTimeKind.Utc;\r\n})\n"})}),"\n",(0,r.jsx)(n.p,{children:"Only one date/time system may be configured per builder instance."}),"\n",(0,r.jsx)(n.h3,{id:"withcommonfactory",children:"WithCommonFactory"}),"\n",(0,r.jsxs)(n.p,{children:["Registers a service type alongside a DI-aware ",(0,r.jsx)(n.code,{children:"ICommonFactory"})," that can create instances through the container."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:".WithCommonFactory()\n"})}),"\n",(0,r.jsx)(n.h2,{id:"persistence-configuration",children:"Persistence configuration"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"WithPersistence"})," accepts any type that implements ",(0,r.jsx)(n.code,{children:"IPersistenceBuilder"}),". The built-in options are ",(0,r.jsx)(n.code,{children:"EFCorePerisistenceBuilder"}),", ",(0,r.jsx)(n.code,{children:"DapperPersistenceBuilder"}),", and ",(0,r.jsx)(n.code,{children:"Linq2DbPersistenceBuilder"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'.WithPersistence(ef =>\r\n{\r\n ef.AddDbContext("AppDb", options =>\r\n {\r\n options.UseSqlServer(connectionString);\r\n });\r\n ef.SetDefaultDataStore(ds =>\r\n {\r\n ds.DefaultDataStoreName = "AppDb";\r\n });\r\n})\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Multiple DbContexts can be registered with different names. The name is used to resolve the correct context at runtime via ",(0,r.jsx)(n.code,{children:"IDataStoreFactory"}),". Use ",(0,r.jsx)(n.code,{children:"SetDefaultDataStore"})," to declare which context a repository uses when no explicit data store name is specified on the repository instance."]}),"\n",(0,r.jsx)(n.h2,{id:"unit-of-work-configuration",children:"Unit of work configuration"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"WithUnitOfWork"})," accepts any type that implements ",(0,r.jsx)(n.code,{children:"IUnitOfWorkBuilder"}),". Use ",(0,r.jsx)(n.code,{children:"DefaultUnitOfWorkBuilder"})," for the standard ",(0,r.jsx)(n.code,{children:"System.Transactions"}),"-based implementation."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:".WithUnitOfWork(uow =>\r\n{\r\n uow.SetOptions(options =>\r\n {\r\n options.AutoCompleteScope = true;\r\n options.DefaultIsolation = IsolationLevel.ReadCommitted;\r\n });\r\n})\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"AutoCompleteScope"})," causes the unit of work scope to commit automatically when the outermost scope completes without an exception. Set it to ",(0,r.jsx)(n.code,{children:"false"})," if you need explicit ",(0,r.jsx)(n.code,{children:"Complete()"})," calls."]}),"\n",(0,r.jsx)(n.h2,{id:"mediator-configuration",children:"Mediator configuration"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"WithMediator"})," accepts any type that implements ",(0,r.jsx)(n.code,{children:"IMediatorBuilder"}),". Use ",(0,r.jsx)(n.code,{children:"MediatRBuilder"})," for MediatR integration."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:".WithMediator(mediator =>\r\n{\r\n mediator.AddRequest();\r\n mediator.AddRequest();\r\n\r\n mediator.Configure(config =>\r\n {\r\n config.RegisterServicesFromAssemblies(typeof(ApplicationAssemblyMarker).Assembly);\r\n });\r\n\r\n mediator.AddLoggingToRequestPipeline();\r\n mediator.AddUnitOfWorkToRequestPipeline();\r\n})\n"})}),"\n",(0,r.jsx)(n.h2,{id:"event-handling-configuration",children:"Event handling configuration"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"WithEventHandling"})," accepts any type that implements ",(0,r.jsx)(n.code,{children:"IEventHandlingBuilder"}),". Use ",(0,r.jsx)(n.code,{children:"InMemoryEventBusBuilder"})," for in-process events."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:".WithEventHandling(eventHandling =>\r\n{\r\n eventHandling.AddSubscriber();\r\n eventHandling.AddSubscriber();\r\n})\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Each ",(0,r.jsx)(n.code,{children:"AddSubscriber"})," call registers the handler as a scoped ",(0,r.jsx)(n.code,{children:"ISubscriber"})," and records the event-to-producer subscription in the ",(0,r.jsx)(n.code,{children:"EventSubscriptionManager"})," so the ",(0,r.jsx)(n.code,{children:"IEventRouter"})," knows which producers handle which events."]}),"\n",(0,r.jsx)(n.h2,{id:"complete-example",children:"Complete example"}),"\n",(0,r.jsxs)(n.p,{children:["The following shows a realistic ",(0,r.jsx)(n.code,{children:"Program.cs"})," configuration for a web API that uses EF Core persistence, MediatR, FluentValidation, and in-process events:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon;\r\nusing RCommon.Persistence.EFCore;\r\nusing RCommon.Persistence.Transactions;\r\nusing RCommon.Mediator.MediatR;\r\nusing RCommon.EventHandling;\r\nusing RCommon.FluentValidation;\r\nusing Microsoft.EntityFrameworkCore;\r\nusing System.Transactions;\r\n\r\nvar builder = WebApplication.CreateBuilder(args);\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithSequentialGuidGenerator(guid =>\r\n guid.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString)\r\n .WithDateTimeSystem(dt =>\r\n dt.Kind = DateTimeKind.Utc)\r\n .WithPersistence(ef =>\r\n {\r\n ef.AddDbContext("AppDb", options =>\r\n options.UseSqlServer(\r\n builder.Configuration.GetConnectionString("DefaultConnection")));\r\n ef.SetDefaultDataStore(ds =>\r\n ds.DefaultDataStoreName = "AppDb");\r\n })\r\n .WithUnitOfWork(uow =>\r\n {\r\n uow.SetOptions(options =>\r\n {\r\n options.AutoCompleteScope = true;\r\n options.DefaultIsolation = IsolationLevel.ReadCommitted;\r\n });\r\n })\r\n .WithMediator(mediator =>\r\n {\r\n mediator.Configure(config =>\r\n config.RegisterServicesFromAssemblies(typeof(ApplicationAssemblyMarker).Assembly));\r\n mediator.AddLoggingToRequestPipeline();\r\n mediator.AddUnitOfWorkToRequestPipeline();\r\n })\r\n .WithEventHandling(events =>\r\n {\r\n events.AddSubscriber();\r\n })\r\n .WithValidation(validation =>\r\n {\r\n validation.AddValidatorsFromAssemblyContaining();\r\n });\n'})}),"\n",(0,r.jsx)(n.h2,{id:"diagnostics",children:"Diagnostics"}),"\n",(0,r.jsx)(n.p,{children:"During development you can print all registered services to the console to verify configuration:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"Console.WriteLine(builder.Services.GenerateServiceDescriptorsString());\n"})}),"\n",(0,r.jsxs)(n.p,{children:["This produces a sorted list of every ",(0,r.jsx)(n.code,{children:"ServiceDescriptor"})," in the container, which is useful for spotting missing or duplicate registrations."]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(l,{...e})}):l(e)}},81753(e,n,i){i.d(n,{R:()=>s,x:()=>d});var r=i(30758);const t={},o=r.createContext(t);function s(e){const n=r.useContext(o);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:s(e.components),r.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/30d27832.362bcc3b.js b/website/build/assets/js/30d27832.362bcc3b.js new file mode 100644 index 00000000..66187821 --- /dev/null +++ b/website/build/assets/js/30d27832.362bcc3b.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[4890],{12397(e,n,i){i.r(n),i.d(n,{assets:()=>c,contentTitle:()=>s,default:()=>h,frontMatter:()=>o,metadata:()=>d,toc:()=>a});var t=i(86070),r=i(81753);const o={title:"Configuration & Bootstrapping",sidebar_position:4,description:"Configure RCommon with AddRCommon() \u2014 fluent builder methods for GUID generation, system time, persistence, unit of work, mediator, and event handling."},s="Configuration & Bootstrapping",d={id:"getting-started/configuration",title:"Configuration & Bootstrapping",description:"Configure RCommon with AddRCommon() \u2014 fluent builder methods for GUID generation, system time, persistence, unit of work, mediator, and event handling.",source:"@site/docs/getting-started/configuration.mdx",sourceDirName:"getting-started",slug:"/getting-started/configuration",permalink:"/docs/next/getting-started/configuration",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/getting-started/configuration.mdx",tags:[],version:"current",sidebarPosition:4,frontMatter:{title:"Configuration & Bootstrapping",sidebar_position:4,description:"Configure RCommon with AddRCommon() \u2014 fluent builder methods for GUID generation, system time, persistence, unit of work, mediator, and event handling."},sidebar:"docsSidebar",previous:{title:"Quick Start Guide",permalink:"/docs/next/getting-started/quick-start"},next:{title:"Dependency Injection",permalink:"/docs/next/getting-started/dependency-injection"}},c={},a=[{value:"The AddRCommon() extension method",id:"the-addrcommon-extension-method",level:2},{value:"Builder chain",id:"builder-chain",level:2},{value:"Core builder methods",id:"core-builder-methods",level:2},{value:"WithSequentialGuidGenerator",id:"withsequentialguidgenerator",level:3},{value:"WithSimpleGuidGenerator",id:"withsimpleguidgenerator",level:3},{value:"WithDateTimeSystem",id:"withdatetimesystem",level:3},{value:"WithCommonFactory",id:"withcommonfactory",level:3},{value:"Persistence configuration",id:"persistence-configuration",level:2},{value:"Unit of work configuration",id:"unit-of-work-configuration",level:2},{value:"Mediator configuration",id:"mediator-configuration",level:2},{value:"Event handling configuration",id:"event-handling-configuration",level:2},{value:"Complete example",id:"complete-example",level:2},{value:"Modular Composition",id:"modular-composition",level:2},{value:"Diagnostics",id:"diagnostics",level:2}];function l(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"configuration--bootstrapping",children:"Configuration & Bootstrapping"}),"\n",(0,t.jsx)(n.p,{children:"RCommon uses a single fluent builder entry point. All configuration happens at application startup before the DI container is built."}),"\n",(0,t.jsx)(n.h2,{id:"the-addrcommon-extension-method",children:"The AddRCommon() extension method"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"AddRCommon()"})," is an extension method on ",(0,t.jsx)(n.code,{children:"IServiceCollection"})," defined in ",(0,t.jsx)(n.code,{children:"RCommon.Core"}),". Calling it creates an ",(0,t.jsx)(n.code,{children:"RCommonBuilder"}),", registers the core framework services, and returns an ",(0,t.jsx)(n.code,{children:"IRCommonBuilder"})," for further configuration."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\n\n// In Program.cs or a DI registration method\nbuilder.Services.AddRCommon();\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Calling ",(0,t.jsx)(n.code,{children:"AddRCommon()"})," always registers these services regardless of what else is configured:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"EventSubscriptionManager"})," (singleton) \u2014 tracks event-to-producer subscriptions"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"IEventBus"})," backed by ",(0,t.jsx)(n.code,{children:"InMemoryEventBus"})," (singleton) \u2014 in-process publish/subscribe"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"IEventRouter"})," backed by ",(0,t.jsx)(n.code,{children:"InMemoryTransactionalEventRouter"})," (scoped) \u2014 coordinates domain events with the unit of work"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"CachingOptions"})," configured with caching disabled by default"]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"builder-chain",children:"Builder chain"}),"\n",(0,t.jsxs)(n.p,{children:["The return value of ",(0,t.jsx)(n.code,{children:"AddRCommon()"})," is ",(0,t.jsx)(n.code,{children:"IRCommonBuilder"}),". Every ",(0,t.jsx)(n.code,{children:"With*"})," method on the builder returns the same ",(0,t.jsx)(n.code,{children:"IRCommonBuilder"})," instance so calls can be chained:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithSequentialGuidGenerator(...)\n .WithDateTimeSystem(...)\n .WithPersistence(...)\n .WithUnitOfWork(...)\n .WithMediator(...)\n .WithEventHandling(...);\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Every ",(0,t.jsx)(n.code,{children:"With*"})," call is independent and opt-in. Omitting a call means those services are not registered."]}),"\n",(0,t.jsx)(n.h2,{id:"core-builder-methods",children:"Core builder methods"}),"\n",(0,t.jsx)(n.h3,{id:"withsequentialguidgenerator",children:"WithSequentialGuidGenerator"}),"\n",(0,t.jsxs)(n.p,{children:["Registers ",(0,t.jsx)(n.code,{children:"IGuidGenerator"})," as a ",(0,t.jsx)(n.code,{children:"SequentialGuidGenerator"}),". Sequential GUIDs are ordered in a way that reduces index fragmentation in SQL databases."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:".WithSequentialGuidGenerator(options =>\n{\n options.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString;\n})\n"})}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"SequentialGuidType"})," values:"]}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Value"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"SequentialAsString"})}),(0,t.jsx)(n.td,{children:"Sequential in string representation \u2014 compatible with MySQL and PostgreSQL"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"SequentialAsBinary"})}),(0,t.jsx)(n.td,{children:"Sequential in binary representation \u2014 compatible with Oracle"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"SequentialAtEnd"})}),(0,t.jsx)(n.td,{children:"Sequential at the end \u2014 compatible with SQL Server"})]})]})]}),"\n",(0,t.jsxs)(n.p,{children:["Only one GUID generator may be configured. Calling ",(0,t.jsx)(n.code,{children:"WithSequentialGuidGenerator"})," or ",(0,t.jsx)(n.code,{children:"WithSimpleGuidGenerator"})," a second time throws ",(0,t.jsx)(n.code,{children:"RCommonBuilderException"}),"."]}),"\n",(0,t.jsx)(n.h3,{id:"withsimpleguidgenerator",children:"WithSimpleGuidGenerator"}),"\n",(0,t.jsxs)(n.p,{children:["Registers ",(0,t.jsx)(n.code,{children:"IGuidGenerator"})," as a ",(0,t.jsx)(n.code,{children:"SimpleGuidGenerator"})," that wraps ",(0,t.jsx)(n.code,{children:"Guid.NewGuid()"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:".WithSimpleGuidGenerator()\n"})}),"\n",(0,t.jsx)(n.h3,{id:"withdatetimesystem",children:"WithDateTimeSystem"}),"\n",(0,t.jsxs)(n.p,{children:["Registers ",(0,t.jsx)(n.code,{children:"ISystemTime"})," as ",(0,t.jsx)(n.code,{children:"SystemTime"}),". Inject ",(0,t.jsx)(n.code,{children:"ISystemTime"})," wherever you need the current time so that tests can substitute a known value."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:".WithDateTimeSystem(options =>\n{\n options.Kind = DateTimeKind.Utc;\n})\n"})}),"\n",(0,t.jsx)(n.p,{children:"Only one date/time system may be configured per builder instance."}),"\n",(0,t.jsx)(n.h3,{id:"withcommonfactory",children:"WithCommonFactory"}),"\n",(0,t.jsxs)(n.p,{children:["Registers a service type alongside a DI-aware ",(0,t.jsx)(n.code,{children:"ICommonFactory"})," that can create instances through the container."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:".WithCommonFactory()\n"})}),"\n",(0,t.jsx)(n.h2,{id:"persistence-configuration",children:"Persistence configuration"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"WithPersistence"})," accepts any type that implements ",(0,t.jsx)(n.code,{children:"IPersistenceBuilder"}),". The built-in options are ",(0,t.jsx)(n.code,{children:"EFCorePerisistenceBuilder"}),", ",(0,t.jsx)(n.code,{children:"DapperPersistenceBuilder"}),", and ",(0,t.jsx)(n.code,{children:"Linq2DbPersistenceBuilder"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'.WithPersistence(ef =>\n{\n ef.AddDbContext("AppDb", options =>\n {\n options.UseSqlServer(connectionString);\n });\n ef.SetDefaultDataStore(ds =>\n {\n ds.DefaultDataStoreName = "AppDb";\n });\n})\n'})}),"\n",(0,t.jsxs)(n.p,{children:["Multiple DbContexts can be registered with different names. The name is used to resolve the correct context at runtime via ",(0,t.jsx)(n.code,{children:"IDataStoreFactory"}),". Use ",(0,t.jsx)(n.code,{children:"SetDefaultDataStore"})," to declare which context a repository uses when no explicit data store name is specified on the repository instance."]}),"\n",(0,t.jsx)(n.h2,{id:"unit-of-work-configuration",children:"Unit of work configuration"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"WithUnitOfWork"})," accepts any type that implements ",(0,t.jsx)(n.code,{children:"IUnitOfWorkBuilder"}),". Use ",(0,t.jsx)(n.code,{children:"DefaultUnitOfWorkBuilder"})," for the standard ",(0,t.jsx)(n.code,{children:"System.Transactions"}),"-based implementation."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:".WithUnitOfWork(uow =>\n{\n uow.SetOptions(options =>\n {\n options.AutoCompleteScope = true;\n options.DefaultIsolation = IsolationLevel.ReadCommitted;\n });\n})\n"})}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"AutoCompleteScope"})," causes the unit of work scope to commit automatically when the outermost scope completes without an exception. Set it to ",(0,t.jsx)(n.code,{children:"false"})," if you need explicit ",(0,t.jsx)(n.code,{children:"Complete()"})," calls."]}),"\n",(0,t.jsx)(n.h2,{id:"mediator-configuration",children:"Mediator configuration"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"WithMediator"})," accepts any type that implements ",(0,t.jsx)(n.code,{children:"IMediatorBuilder"}),". Use ",(0,t.jsx)(n.code,{children:"MediatRBuilder"})," for MediatR integration."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:".WithMediator(mediator =>\n{\n mediator.AddRequest();\n mediator.AddRequest();\n\n mediator.Configure(config =>\n {\n config.RegisterServicesFromAssemblies(typeof(ApplicationAssemblyMarker).Assembly);\n });\n\n mediator.AddLoggingToRequestPipeline();\n mediator.AddUnitOfWorkToRequestPipeline();\n})\n"})}),"\n",(0,t.jsx)(n.h2,{id:"event-handling-configuration",children:"Event handling configuration"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"WithEventHandling"})," accepts any type that implements ",(0,t.jsx)(n.code,{children:"IEventHandlingBuilder"}),". Use ",(0,t.jsx)(n.code,{children:"InMemoryEventBusBuilder"})," for in-process events."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:".WithEventHandling(eventHandling =>\n{\n eventHandling.AddSubscriber();\n eventHandling.AddSubscriber();\n})\n"})}),"\n",(0,t.jsxs)(n.p,{children:["Each ",(0,t.jsx)(n.code,{children:"AddSubscriber"})," call registers the handler as a scoped ",(0,t.jsx)(n.code,{children:"ISubscriber"})," and records the event-to-producer subscription in the ",(0,t.jsx)(n.code,{children:"EventSubscriptionManager"})," so the ",(0,t.jsx)(n.code,{children:"IEventRouter"})," knows which producers handle which events."]}),"\n",(0,t.jsx)(n.h2,{id:"complete-example",children:"Complete example"}),"\n",(0,t.jsxs)(n.p,{children:["The following shows a realistic ",(0,t.jsx)(n.code,{children:"Program.cs"})," configuration for a web API that uses EF Core persistence, MediatR, FluentValidation, and in-process events:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'using RCommon;\nusing RCommon.Persistence.EFCore;\nusing RCommon.Persistence.Transactions;\nusing RCommon.Mediator.MediatR;\nusing RCommon.EventHandling;\nusing RCommon.FluentValidation;\nusing Microsoft.EntityFrameworkCore;\nusing System.Transactions;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddRCommon()\n .WithSequentialGuidGenerator(guid =>\n guid.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString)\n .WithDateTimeSystem(dt =>\n dt.Kind = DateTimeKind.Utc)\n .WithPersistence(ef =>\n {\n ef.AddDbContext("AppDb", options =>\n options.UseSqlServer(\n builder.Configuration.GetConnectionString("DefaultConnection")));\n ef.SetDefaultDataStore(ds =>\n ds.DefaultDataStoreName = "AppDb");\n })\n .WithUnitOfWork(uow =>\n {\n uow.SetOptions(options =>\n {\n options.AutoCompleteScope = true;\n options.DefaultIsolation = IsolationLevel.ReadCommitted;\n });\n })\n .WithMediator(mediator =>\n {\n mediator.Configure(config =>\n config.RegisterServicesFromAssemblies(typeof(ApplicationAssemblyMarker).Assembly));\n mediator.AddLoggingToRequestPipeline();\n mediator.AddUnitOfWorkToRequestPipeline();\n })\n .WithEventHandling(events =>\n {\n events.AddSubscriber();\n })\n .WithValidation(validation =>\n {\n validation.AddValidatorsFromAssemblyContaining();\n });\n'})}),"\n",(0,t.jsx)(n.h2,{id:"modular-composition",children:"Modular Composition"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"AddRCommon()"})," is idempotent across modules. The first call constructs the ",(0,t.jsx)(n.code,{children:"IRCommonBuilder"})," and caches it on ",(0,t.jsx)(n.code,{children:"IServiceCollection"}),"; every subsequent call returns the same instance. Sub-builder verbs (",(0,t.jsx)(n.code,{children:"WithPersistence"}),", ",(0,t.jsx)(n.code,{children:"WithMediator"}),", ",(0,t.jsx)(n.code,{children:"WithEventHandling"}),", ",(0,t.jsx)(n.code,{children:"WithUnitOfWork"}),", ",(0,t.jsx)(n.code,{children:"WithCQRS"}),", ",(0,t.jsx)(n.code,{children:"WithValidation"}),", ",(0,t.jsx)(n.code,{children:"WithBlobStorage"}),", ",(0,t.jsx)(n.code,{children:"WithMultiTenancy"}),", ",(0,t.jsx)(n.code,{children:"WithMemoryCaching"}),", ",(0,t.jsx)(n.code,{children:"WithDistributedCaching"}),") are cache-aware: a second call with the same ",(0,t.jsx)(n.code,{children:"T"})," reuses the cached sub-builder, so the supplied ",(0,t.jsx)(n.code,{children:"Action"})," runs against the same instance and registrations accumulate. Singleton-style verbs (",(0,t.jsx)(n.code,{children:"WithSimpleGuidGenerator"}),", ",(0,t.jsx)(n.code,{children:"WithSequentialGuidGenerator"}),", ",(0,t.jsx)(n.code,{children:"WithDateTimeSystem"}),", ",(0,t.jsx)(n.code,{children:"WithJsonSerialization"}),", ",(0,t.jsx)(n.code,{children:"WithSmtpEmailServices"}),", ",(0,t.jsx)(n.code,{children:"WithSendGridEmailServices"}),") are idempotent on identical impls and throw ",(0,t.jsx)(n.code,{children:"RCommonBuilderException"})," only when a different impl competes for the same slot."]}),"\n",(0,t.jsxs)(n.p,{children:["The typical pattern: each module exposes a ",(0,t.jsx)(n.code,{children:"Configure(IServiceCollection services)"})," method that builds its slice of RCommon independently."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'public interface IServiceModule\n{\n void Configure(IServiceCollection services);\n}\n\npublic sealed class OrderingModule : IServiceModule\n{\n public void Configure(IServiceCollection services)\n {\n services.AddRCommon()\n .WithSimpleGuidGenerator()\n .WithPersistence(ef =>\n ef.AddDbContext(\n "Ordering",\n o => o.UseSqlServer(orderingConnectionString)));\n }\n}\n\npublic sealed class InventoryModule : IServiceModule\n{\n public void Configure(IServiceCollection services)\n {\n services.AddRCommon()\n .WithSimpleGuidGenerator() // Same impl as Ordering \u2014 idempotent no-op.\n .WithPersistence(ef =>\n ef.AddDbContext(\n "Inventory",\n o => o.UseNpgsql(inventoryConnectionString)));\n }\n}\n'})}),"\n",(0,t.jsxs)(n.p,{children:["Each module pulls in only the features it needs; the framework deduplicates shared registrations automatically. See ",(0,t.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix and the runnable example under ",(0,t.jsx)(n.code,{children:"Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/"}),"."]}),"\n",(0,t.jsx)(n.h2,{id:"diagnostics",children:"Diagnostics"}),"\n",(0,t.jsx)(n.p,{children:"During development you can print all registered services to the console to verify configuration:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"Console.WriteLine(builder.Services.GenerateServiceDescriptorsString());\n"})}),"\n",(0,t.jsxs)(n.p,{children:["This produces a sorted list of every ",(0,t.jsx)(n.code,{children:"ServiceDescriptor"})," in the container, which is useful for spotting missing or duplicate registrations."]})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(l,{...e})}):l(e)}},81753(e,n,i){i.d(n,{R:()=>s,x:()=>d});var t=i(30758);const r={},o=t.createContext(r);function s(e){const n=t.useContext(o);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),t.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/364ef54b.04095105.js b/website/build/assets/js/364ef54b.04095105.js deleted file mode 100644 index cf24e6ca..00000000 --- a/website/build/assets/js/364ef54b.04095105.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[2704],{52054(e,n,a){a.r(n),a.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>u,frontMatter:()=>i,metadata:()=>o,toc:()=>d});var r=a(86070),t=a(81753);a(85363),a(64996);const i={title:"HR Leave Management Sample",sidebar_position:1,description:"Explore RCommon's HR Leave Management reference app demonstrating Clean Architecture, CQRS, EF Core, FluentValidation, JWT auth, and SendGrid email integration."},s="HR Leave Management Sample",o={id:"examples-recipes/hr-leave-management",title:"HR Leave Management Sample",description:"Explore RCommon's HR Leave Management reference app demonstrating Clean Architecture, CQRS, EF Core, FluentValidation, JWT auth, and SendGrid email integration.",source:"@site/docs/examples-recipes/hr-leave-management.mdx",sourceDirName:"examples-recipes",slug:"/examples-recipes/hr-leave-management",permalink:"/docs/next/examples-recipes/hr-leave-management",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/examples-recipes/hr-leave-management.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"HR Leave Management Sample",sidebar_position:1,description:"Explore RCommon's HR Leave Management reference app demonstrating Clean Architecture, CQRS, EF Core, FluentValidation, JWT auth, and SendGrid email integration."},sidebar:"docsSidebar",previous:{title:"Examples & Recipes",permalink:"/docs/next/category/examples--recipes"},next:{title:"Event Handling Examples",permalink:"/docs/next/examples-recipes/event-handling"}},l={},d=[{value:"Sample Location",id:"sample-location",level:2},{value:"What the Sample Covers",id:"what-the-sample-covers",level:2},{value:"Domain Model",id:"domain-model",level:2},{value:"Entities",id:"entities",level:3},{value:"Domain Specifications",id:"domain-specifications",level:3},{value:"Application Layer",id:"application-layer",level:2},{value:"Commands and Queries",id:"commands-and-queries",level:3},{value:"Command Handler with Validation",id:"command-handler-with-validation",level:3},{value:"Command Handler with Multiple Repositories and Email",id:"command-handler-with-multiple-repositories-and-email",level:3},{value:"Allocation Command Handler",id:"allocation-command-handler",level:3},{value:"Validation",id:"validation",level:3},{value:"Persistence Layer",id:"persistence-layer",level:2},{value:"API Controllers",id:"api-controllers",level:2},{value:"Composition Root",id:"composition-root",level:2},{value:"Unit Testing Handlers",id:"unit-testing-handlers",level:2},{value:"Key Takeaways",id:"key-takeaways",level:2}];function c(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,t.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"hr-leave-management-sample",children:"HR Leave Management Sample"}),"\n",(0,r.jsx)(n.p,{children:"The HR Leave Management sample is the reference application included with RCommon. It demonstrates Clean Architecture, CQRS with MediatR, EF Core persistence, FluentValidation, JWT authentication, and SendGrid email \u2014 all wired together through RCommon's fluent builder. This walkthrough explains how each piece fits together."}),"\n",(0,r.jsx)(n.h2,{id:"sample-location",children:"Sample Location"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"Examples/CleanWithCQRS/\r\n HR.LeaveManagement.Domain/\r\n HR.LeaveManagement.Application/\r\n HR.LeaveManagement.Persistence/\r\n HR.LeaveManagement.Identity/\r\n HR.LeaveManagement.API/\r\n HR.LeaveManagement.MVC/\r\n HR.LeaveManagement.Application.UnitTests/\n"})}),"\n",(0,r.jsx)(n.h2,{id:"what-the-sample-covers",children:"What the Sample Covers"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"A leave management system where employees request time off"}),"\n",(0,r.jsx)(n.li,{children:"Administrators create leave types (e.g. Annual, Sick, Bereavement) and allocate days to employees"}),"\n",(0,r.jsx)(n.li,{children:"Employees submit leave requests against their allocations"}),"\n",(0,r.jsx)(n.li,{children:"Requests can be approved or rejected by administrators"}),"\n",(0,r.jsx)(n.li,{children:"Email confirmation is sent on successful request submission"}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"domain-model",children:"Domain Model"}),"\n",(0,r.jsx)(n.h3,{id:"entities",children:"Entities"}),"\n",(0,r.jsxs)(n.p,{children:["All entities inherit from ",(0,r.jsx)(n.code,{children:"BaseDomainEntity"}),", which inherits from RCommon's ",(0,r.jsx)(n.code,{children:"AuditedEntity"}),". This provides ",(0,r.jsx)(n.code,{children:"Id"}),", ",(0,r.jsx)(n.code,{children:"CreatedBy"}),", ",(0,r.jsx)(n.code,{children:"ModifiedBy"}),", ",(0,r.jsx)(n.code,{children:"CreateDate"}),", and ",(0,r.jsx)(n.code,{children:"ModifyDate"})," automatically:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'public abstract class BaseDomainEntity : AuditedEntity\r\n{\r\n}\r\n\r\npublic class LeaveType : BaseDomainEntity\r\n{\r\n public string Name { get; set; } // e.g. "Annual Leave"\r\n public int DefaultDays { get; set; } // e.g. 14\r\n}\r\n\r\npublic class LeaveAllocation : BaseDomainEntity\r\n{\r\n public int NumberOfDays { get; set; }\r\n public LeaveType LeaveType { get; set; }\r\n public int LeaveTypeId { get; set; }\r\n public int Period { get; set; } // year\r\n public string EmployeeId { get; set; }\r\n}\r\n\r\npublic class LeaveRequest : BaseDomainEntity\r\n{\r\n public DateTime StartDate { get; set; }\r\n public DateTime EndDate { get; set; }\r\n public LeaveType LeaveType { get; set; }\r\n public int LeaveTypeId { get; set; }\r\n public DateTime DateRequested { get; set; }\r\n public string RequestComments { get; set; }\r\n public bool? Approved { get; set; }\r\n public bool Cancelled { get; set; }\r\n public string RequestingEmployeeId { get; set; }\r\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"domain-specifications",children:"Domain Specifications"}),"\n",(0,r.jsxs)(n.p,{children:["A specification encapsulates a reusable query predicate. The ",(0,r.jsx)(n.code,{children:"AllocationExistsSpec"})," checks whether an employee already has an allocation for a given leave type in a given year:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"public class AllocationExistsSpec : Specification\r\n{\r\n public AllocationExistsSpec(string userId, int leaveTypeId, int period)\r\n : base(q => q.EmployeeId == userId\r\n && q.LeaveTypeId == leaveTypeId\r\n && q.Period == period)\r\n {\r\n }\r\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:"Usage in a command handler:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"var allocationCount = await _leaveAllocationRepository\r\n .GetCountAsync(new AllocationExistsSpec(emp.Id, leaveType.Id, period));\r\n\r\nif (allocationCount > 0)\r\n continue; // already allocated for this period\n"})}),"\n",(0,r.jsx)(n.h2,{id:"application-layer",children:"Application Layer"}),"\n",(0,r.jsx)(n.h3,{id:"commands-and-queries",children:"Commands and Queries"}),"\n",(0,r.jsxs)(n.p,{children:["CQRS is expressed through RCommon's ",(0,r.jsx)(n.code,{children:"IAppRequest"})," and ",(0,r.jsx)(n.code,{children:"IAppRequestHandler"})," interfaces."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"// Command \u2014 modifies state\r\npublic class CreateLeaveTypeCommand : IAppRequest\r\n{\r\n public CreateLeaveTypeDto LeaveTypeDto { get; set; }\r\n}\r\n\r\n// Query \u2014 reads state, no side effects\r\npublic class GetLeaveTypeListRequest : IAppRequest>\r\n{\r\n}\n"})}),"\n",(0,r.jsx)(n.h3,{id:"command-handler-with-validation",children:"Command Handler with Validation"}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"CreateLeaveTypeCommandHandler"})," validates the incoming DTO via ",(0,r.jsx)(n.code,{children:"IValidationService"})," before persisting:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'public class CreateLeaveTypeCommandHandler\r\n : IAppRequestHandler\r\n{\r\n private readonly IGraphRepository _leaveTypeRepository;\r\n private readonly IValidationService _validationService;\r\n\r\n public CreateLeaveTypeCommandHandler(\r\n IGraphRepository leaveTypeRepository,\r\n IValidationService validationService)\r\n {\r\n _leaveTypeRepository = leaveTypeRepository;\r\n _validationService = validationService;\r\n _leaveTypeRepository.DataStoreName = DataStoreNamesConst.LeaveManagement;\r\n }\r\n\r\n public async Task HandleAsync(\r\n CreateLeaveTypeCommand request,\r\n CancellationToken cancellationToken)\r\n {\r\n var response = new BaseCommandResponse();\r\n var validationResult = await _validationService.ValidateAsync(request.LeaveTypeDto);\r\n\r\n if (!validationResult.IsValid)\r\n {\r\n response.Success = false;\r\n response.Message = "Creation Failed";\r\n response.Errors = validationResult.Errors\r\n .Select(q => q.ErrorMessage).ToList();\r\n }\r\n else\r\n {\r\n var leaveType = request.LeaveTypeDto.ToLeaveType();\r\n await _leaveTypeRepository.AddAsync(leaveType);\r\n response.Success = true;\r\n response.Message = "Creation Successful";\r\n response.Id = leaveType.Id;\r\n }\r\n\r\n return response;\r\n }\r\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"command-handler-with-multiple-repositories-and-email",children:"Command Handler with Multiple Repositories and Email"}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"CreateLeaveRequestCommandHandler"})," coordinates multiple repositories, the current user identity, and email delivery. The handler remains unit-testable because all dependencies are injected as interfaces:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'public class CreateLeaveRequestCommandHandler\r\n : IAppRequestHandler\r\n{\r\n private readonly IReadOnlyRepository _leaveTypeRepository;\r\n private readonly IGraphRepository _leaveAllocationRepository;\r\n private readonly IGraphRepository _leaveRequestRepository;\r\n private readonly IEmailService _emailSender;\r\n private readonly ICurrentUser _currentUser;\r\n private readonly IValidationService _validationService;\r\n\r\n public async Task HandleAsync(\r\n CreateLeaveRequestCommand request,\r\n CancellationToken cancellationToken)\r\n {\r\n var response = new BaseCommandResponse();\r\n var validationResult = await _validationService.ValidateAsync(request.LeaveRequestDto);\r\n var userId = _currentUser.FindClaimValue(CustomClaimTypes.Uid);\r\n\r\n // Check allocation exists\r\n var allocation = _leaveAllocationRepository.FirstOrDefault(\r\n x => x.EmployeeId == userId\r\n && x.LeaveTypeId == request.LeaveRequestDto.LeaveTypeId);\r\n\r\n if (allocation is null)\r\n {\r\n validationResult.Errors.Add(new ValidationFault(\r\n nameof(request.LeaveRequestDto.LeaveTypeId),\r\n "You do not have any allocations for this leave type."));\r\n }\r\n else\r\n {\r\n int daysRequested = (int)(request.LeaveRequestDto.EndDate\r\n - request.LeaveRequestDto.StartDate).TotalDays;\r\n if (daysRequested > allocation.NumberOfDays)\r\n {\r\n validationResult.Errors.Add(new ValidationFault(\r\n nameof(request.LeaveRequestDto.EndDate),\r\n "You do not have enough days for this request"));\r\n }\r\n }\r\n\r\n if (!validationResult.IsValid)\r\n {\r\n response.Success = false;\r\n response.Message = "Request Failed";\r\n response.Errors = validationResult.Errors\r\n .Select(q => q.ErrorMessage).ToList();\r\n }\r\n else\r\n {\r\n var leaveRequest = request.LeaveRequestDto.ToLeaveRequest();\r\n leaveRequest.RequestingEmployeeId = userId;\r\n await _leaveRequestRepository.AddAsync(leaveRequest);\r\n\r\n response.Success = true;\r\n response.Message = "Request Created Successfully";\r\n response.Id = leaveRequest.Id;\r\n\r\n // Send confirmation email \u2014 failure does not roll back the request\r\n try\r\n {\r\n var emailAddress = _currentUser.FindClaimValue(ClaimTypes.Email);\r\n var email = new MailMessage(\r\n new MailAddress(fromEmail, fromName),\r\n new MailAddress(emailAddress))\r\n {\r\n Subject = "Leave Request Submitted",\r\n Body = $"Your leave request for {request.LeaveRequestDto.StartDate:D} " +\r\n $"to {request.LeaveRequestDto.EndDate:D} has been submitted."\r\n };\r\n await _emailSender.SendEmailAsync(email);\r\n }\r\n catch (Exception)\r\n {\r\n // Log but do not propagate \u2014 email is non-critical\r\n }\r\n }\r\n\r\n return response;\r\n }\r\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"allocation-command-handler",children:"Allocation Command Handler"}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"CreateLeaveAllocationCommandHandler"})," demonstrates using a specification to avoid duplicate allocations. It retrieves all employees via the ",(0,r.jsx)(n.code,{children:"IUserService"})," contract and allocates days from the leave type's ",(0,r.jsx)(n.code,{children:"DefaultDays"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'public async Task HandleAsync(\r\n CreateLeaveAllocationCommand request,\r\n CancellationToken cancellationToken)\r\n{\r\n var leaveType = await _leaveTypeRepository\r\n .FindAsync(request.LeaveAllocationDto.LeaveTypeId);\r\n var employees = await _userService.GetEmployees();\r\n var period = DateTime.Now.Year;\r\n var allocations = new List();\r\n\r\n foreach (var emp in employees)\r\n {\r\n var allocationCount = await _leaveAllocationRepository\r\n .GetCountAsync(new AllocationExistsSpec(emp.Id, leaveType.Id, period));\r\n\r\n if (allocationCount > 0)\r\n continue;\r\n\r\n allocations.Add(new LeaveAllocation\r\n {\r\n EmployeeId = emp.Id,\r\n LeaveTypeId = leaveType.Id,\r\n NumberOfDays = leaveType.DefaultDays,\r\n Period = period\r\n });\r\n }\r\n\r\n foreach (var item in allocations)\r\n {\r\n await _leaveAllocationRepository.AddAsync(item);\r\n }\r\n\r\n return new BaseCommandResponse { Success = true, Message = "Allocations Successful" };\r\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"validation",children:"Validation"}),"\n",(0,r.jsxs)(n.p,{children:["Validators use FluentValidation. They are discovered automatically via ",(0,r.jsx)(n.code,{children:"AddValidatorsFromAssemblyContaining"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'public class ILeaveTypeDtoValidator : AbstractValidator\r\n{\r\n public ILeaveTypeDtoValidator()\r\n {\r\n RuleFor(p => p.Name)\r\n .NotEmpty().WithMessage("{PropertyName} is required.")\r\n .NotNull()\r\n .MaximumLength(50)\r\n .WithMessage("{PropertyName} must not exceed {ComparisonValue} characters.");\r\n\r\n RuleFor(p => p.DefaultDays)\r\n .NotEmpty().WithMessage("{PropertyName} is required.")\r\n .GreaterThan(0).WithMessage("{PropertyName} must be at least 1.")\r\n .LessThan(100)\r\n .WithMessage("{PropertyName} must be less than {ComparisonValue}.");\r\n }\r\n}\r\n\r\npublic class CreateLeaveTypeDtoValidator : AbstractValidator\r\n{\r\n public CreateLeaveTypeDtoValidator()\r\n {\r\n Include(new ILeaveTypeDtoValidator());\r\n }\r\n}\n'})}),"\n",(0,r.jsx)(n.h2,{id:"persistence-layer",children:"Persistence Layer"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"LeaveManagementDbContext"})," inherits from ",(0,r.jsx)(n.code,{children:"AuditableDbContext"}),". RCommon injects ",(0,r.jsx)(n.code,{children:"ICurrentUser"})," and ",(0,r.jsx)(n.code,{children:"ISystemTime"})," to stamp audit fields on every ",(0,r.jsx)(n.code,{children:"SaveChanges"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"public class LeaveManagementDbContext : AuditableDbContext\r\n{\r\n public LeaveManagementDbContext(\r\n DbContextOptions options,\r\n ICurrentUser currentUser,\r\n ISystemTime systemTime)\r\n : base(options, currentUser, systemTime)\r\n {\r\n }\r\n\r\n protected override void OnModelCreating(ModelBuilder modelBuilder)\r\n {\r\n modelBuilder.ApplyConfigurationsFromAssembly(\r\n typeof(LeaveManagementDbContext).Assembly);\r\n }\r\n\r\n public DbSet LeaveRequests { get; set; }\r\n public DbSet LeaveTypes { get; set; }\r\n public DbSet LeaveAllocations { get; set; }\r\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:"The data store name constant keeps the name in one place:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'public static class DataStoreNamesConst\r\n{\r\n public const string LeaveManagement = "LeaveManagement";\r\n}\n'})}),"\n",(0,r.jsx)(n.h2,{id:"api-controllers",children:"API Controllers"}),"\n",(0,r.jsxs)(n.p,{children:["Controllers use ",(0,r.jsx)(n.code,{children:"IMediatorService"})," exclusively \u2014 no direct handler references:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'[Route("api/[controller]")]\r\n[ApiController]\r\n[Authorize]\r\npublic class LeaveTypesController : ControllerBase\r\n{\r\n private readonly IMediatorService _mediator;\r\n\r\n public LeaveTypesController(IMediatorService mediator)\r\n => _mediator = mediator;\r\n\r\n [HttpGet]\r\n public async Task>> Get()\r\n {\r\n var leaveTypes = await _mediator.Send>(\r\n new GetLeaveTypeListRequest());\r\n return Ok(leaveTypes);\r\n }\r\n\r\n [HttpPost]\r\n [Authorize(Roles = "Administrator")]\r\n public async Task> Post(\r\n [FromBody] CreateLeaveTypeDto leaveType)\r\n {\r\n var command = new CreateLeaveTypeCommand { LeaveTypeDto = leaveType };\r\n var response = await _mediator.Send(command);\r\n return Ok(response);\r\n }\r\n\r\n [HttpPut("{id}")]\r\n [Authorize(Roles = "Administrator")]\r\n public async Task Put([FromBody] LeaveTypeDto leaveType)\r\n {\r\n var command = new UpdateLeaveTypeCommand { LeaveTypeDto = leaveType };\r\n await _mediator.Send(command);\r\n return NoContent();\r\n }\r\n\r\n [HttpDelete("{id}")]\r\n [Authorize(Roles = "Administrator")]\r\n public async Task Delete(int id)\r\n {\r\n var command = new DeleteLeaveTypeCommand { Id = id };\r\n await _mediator.Send(command);\r\n return NoContent();\r\n }\r\n}\n'})}),"\n",(0,r.jsx)(n.h2,{id:"composition-root",children:"Composition Root"}),"\n",(0,r.jsxs)(n.p,{children:["Everything is wired in ",(0,r.jsx)(n.code,{children:"Program.cs"}),". This is the only place in the system where concrete implementations are referenced by name:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithClaimsAndPrincipalAccessor()\r\n .WithSendGridEmailServices(x =>\r\n {\r\n var settings = builder.Configuration.Get();\r\n x.SendGridApiKey = settings.SendGridApiKey;\r\n x.FromNameDefault = settings.FromNameDefault;\r\n x.FromEmailDefault = settings.FromEmailDefault;\r\n })\r\n .WithDateTimeSystem(dateTime => dateTime.Kind = DateTimeKind.Utc)\r\n .WithSequentialGuidGenerator(guid =>\r\n guid.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString)\r\n .WithMediator(mediator =>\r\n {\r\n mediator.AddRequest();\r\n mediator.AddRequest,\r\n GetLeaveTypeListRequestHandler>();\r\n // ... all other request/handler pairs\r\n\r\n mediator.Configure(config =>\r\n {\r\n config.RegisterServicesFromAssemblies(\r\n typeof(ApplicationServicesRegistration).GetTypeInfo().Assembly);\r\n });\r\n mediator.AddLoggingToRequestPipeline();\r\n mediator.AddUnitOfWorkToRequestPipeline();\r\n })\r\n .WithPersistence(ef =>\r\n {\r\n ef.AddDbContext(\r\n DataStoreNamesConst.LeaveManagement,\r\n options => options.UseSqlServer(\r\n builder.Configuration.GetConnectionString(\r\n DataStoreNamesConst.LeaveManagement)));\r\n ef.SetDefaultDataStore(dataStore =>\r\n dataStore.DefaultDataStoreName = DataStoreNamesConst.LeaveManagement);\r\n })\r\n .WithUnitOfWork(unitOfWork =>\r\n {\r\n unitOfWork.SetOptions(options =>\r\n {\r\n options.AutoCompleteScope = true;\r\n options.DefaultIsolation = IsolationLevel.ReadCommitted;\r\n });\r\n })\r\n .WithValidation(validation =>\r\n {\r\n validation.AddValidatorsFromAssemblyContaining(\r\n typeof(ApplicationServicesRegistration));\r\n });\n"})}),"\n",(0,r.jsx)(n.h2,{id:"unit-testing-handlers",children:"Unit Testing Handlers"}),"\n",(0,r.jsx)(n.p,{children:"Because every handler depends on interfaces, tests use mocks and require no database:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'[TestFixture]\r\npublic class CreateLeaveTypeCommandHandlerTests\r\n{\r\n private readonly CreateLeaveTypeDto _leaveTypeDto;\r\n private readonly CreateLeaveTypeCommandHandler _handler;\r\n\r\n public CreateLeaveTypeCommandHandlerTests()\r\n {\r\n var repositoryMock = new Mock>();\r\n var validationMock = new Mock();\r\n\r\n _leaveTypeDto = new CreateLeaveTypeDto\r\n {\r\n DefaultDays = 15,\r\n Name = "Test DTO"\r\n };\r\n\r\n validationMock\r\n .Setup(x => x.ValidateAsync(_leaveTypeDto, false, CancellationToken.None))\r\n .Returns(() => Task.FromResult(new ValidationOutcome()));\r\n\r\n _handler = new CreateLeaveTypeCommandHandler(\r\n repositoryMock.Object,\r\n validationMock.Object);\r\n }\r\n\r\n [Test]\r\n public async Task Valid_LeaveType_Added()\r\n {\r\n var result = await _handler.HandleAsync(\r\n new CreateLeaveTypeCommand { LeaveTypeDto = _leaveTypeDto },\r\n CancellationToken.None);\r\n\r\n result.ShouldBeOfType();\r\n result.Success.ShouldBeTrue();\r\n }\r\n}\n'})}),"\n",(0,r.jsx)(n.h2,{id:"key-takeaways",children:"Key Takeaways"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:(0,r.jsx)(n.code,{children:"IGraphRepository"})})," decouples handlers from the ORM. Switch from EF Core to NHibernate without touching a single handler."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:(0,r.jsx)(n.code,{children:"IValidationService"})})," decouples handlers from FluentValidation. The validator registration is a composition-root concern."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:(0,r.jsx)(n.code,{children:"ICurrentUser"})})," provides the authenticated user identity without importing ASP.NET Core into the domain or application layers."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:(0,r.jsx)(n.code,{children:"AuditableDbContext"})})," eliminates boilerplate audit stamping across every save."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Pipeline behaviors"})," (",(0,r.jsx)(n.code,{children:"AddLoggingToRequestPipeline"}),", ",(0,r.jsx)(n.code,{children:"AddUnitOfWorkToRequestPipeline"}),") apply cross-cutting concerns to every handler without modifying handler code."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:(0,r.jsx)(n.code,{children:"DataStoreName"})})," enables routing to the correct ",(0,r.jsx)(n.code,{children:"DbContext"})," when multiple databases are registered."]}),"\n"]})]})}function u(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},64996(e,n,a){a.d(n,{A:()=>s});a(30758);var r=a(13526);const t="tabItem_jA3u";var i=a(86070);function s({children:e,hidden:n,className:a}){return(0,i.jsx)("div",{role:"tabpanel",className:(0,r.A)(t,a),hidden:n,children:e})}},85363(e,n,a){a.d(n,{A:()=>R});var r=a(30758),t=a(13526),i=a(32416),s=a(25557),o=a(85924),l=a(64493),d=a(10274),c=a(44118);function u(e){return r.Children.toArray(e).filter(e=>"\n"!==e).map(e=>{if(!e||(0,r.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)})?.filter(Boolean)??[]}function p(e){const{values:n,children:a}=e;return(0,r.useMemo)(()=>{const e=n??function(e){return u(e).map(({props:{value:e,label:n,attributes:a,default:r}})=>({value:e,label:n,attributes:a,default:r}))}(a);return function(e){const n=(0,d.X)(e,(e,n)=>e.value===n.value);if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map(e=>e.value).join(", ")}" found in . Every value needs to be unique.`)}(e),e},[n,a])}function m({value:e,tabValues:n}){return n.some(n=>n.value===e)}function v({queryString:e=!1,groupId:n}){const a=(0,s.W6)(),t=function({queryString:e=!1,groupId:n}){if("string"==typeof e)return e;if(!1===e)return null;if(!0===e&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:e,groupId:n});return[(0,l.aZ)(t),(0,r.useCallback)(e=>{if(!t)return;const n=new URLSearchParams(a.location.search);n.set(t,e),a.replace({...a.location,search:n.toString()})},[t,a])]}function h(e){const{defaultValue:n,queryString:a=!1,groupId:t}=e,i=p(e),[s,l]=(0,r.useState)(()=>function({defaultValue:e,tabValues:n}){if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(e){if(!m({value:e,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${e}" but none of its children has the corresponding value. Available values are: ${n.map(e=>e.value).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return e}const a=n.find(e=>e.default)??n[0];if(!a)throw new Error("Unexpected error: 0 tabValues");return a.value}({defaultValue:n,tabValues:i})),[d,u]=v({queryString:a,groupId:t}),[h,y]=function({groupId:e}){const n=function(e){return e?`docusaurus.tab.${e}`:null}(e),[a,t]=(0,c.Dv)(n);return[a,(0,r.useCallback)(e=>{n&&t.set(e)},[n,t])]}({groupId:t}),g=(()=>{const e=d??h;return m({value:e,tabValues:i})?e:null})();(0,o.A)(()=>{g&&l(g)},[g]);return{selectedValue:s,selectValue:(0,r.useCallback)(e=>{if(!m({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);l(e),u(e),y(e)},[u,y,i]),tabValues:i}}var y=a(81264);const g="tabList_TfjZ",T="tabItem_vVs9";var x=a(86070);function C({className:e,block:n,selectedValue:a,selectValue:r,tabValues:s}){const o=[],{blockElementScrollPositionUntilNextRender:l}=(0,i.a_)(),d=e=>{const n=e.currentTarget,t=o.indexOf(n),i=s[t].value;i!==a&&(l(n),r(i))},c=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const a=o.indexOf(e.currentTarget)+1;n=o[a]??o[0];break}case"ArrowLeft":{const a=o.indexOf(e.currentTarget)-1;n=o[a]??o[o.length-1];break}}n?.focus()};return(0,x.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,t.A)("tabs",{"tabs--block":n},e),children:s.map(({value:e,label:n,attributes:r})=>(0,x.jsx)("li",{role:"tab",tabIndex:a===e?0:-1,"aria-selected":a===e,ref:e=>o.push(e),onKeyDown:c,onClick:d,...r,className:(0,t.A)("tabs__item",T,r?.className,{"tabs__item--active":a===e}),children:n??e},e))})}function b({lazy:e,children:n,selectedValue:a}){const t=(Array.isArray(n)?n:[n]).filter(Boolean);if(e){const e=t.find(e=>e.props.value===a);return e?(0,r.cloneElement)(e,{className:"margin-top--md"}):null}return(0,x.jsx)("div",{className:"margin-top--md",children:t.map((e,n)=>(0,r.cloneElement)(e,{key:n,hidden:e.props.value!==a}))})}function f(e){const n=h(e);return(0,x.jsxs)("div",{className:(0,t.A)("tabs-container",g),children:[(0,x.jsx)(C,{...n,...e}),(0,x.jsx)(b,{...n,...e})]})}function R(e){const n=(0,y.A)();return(0,x.jsx)(f,{...e,children:u(e.children)},String(n))}},81753(e,n,a){a.d(n,{R:()=>s,x:()=>o});var r=a(30758);const t={},i=r.createContext(t);function s(e){const n=r.useContext(i);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:s(e.components),r.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/364ef54b.3133a7a2.js b/website/build/assets/js/364ef54b.3133a7a2.js new file mode 100644 index 00000000..7eaa75a9 --- /dev/null +++ b/website/build/assets/js/364ef54b.3133a7a2.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[2704],{52054(e,n,a){a.r(n),a.d(n,{assets:()=>l,contentTitle:()=>r,default:()=>u,frontMatter:()=>s,metadata:()=>o,toc:()=>d});var t=a(86070),i=a(81753);a(85363),a(64996);const s={title:"HR Leave Management Sample",sidebar_position:1,description:"Explore RCommon's HR Leave Management reference app demonstrating Clean Architecture, CQRS, EF Core, FluentValidation, JWT auth, and SendGrid email integration."},r="HR Leave Management Sample",o={id:"examples-recipes/hr-leave-management",title:"HR Leave Management Sample",description:"Explore RCommon's HR Leave Management reference app demonstrating Clean Architecture, CQRS, EF Core, FluentValidation, JWT auth, and SendGrid email integration.",source:"@site/docs/examples-recipes/hr-leave-management.mdx",sourceDirName:"examples-recipes",slug:"/examples-recipes/hr-leave-management",permalink:"/docs/next/examples-recipes/hr-leave-management",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/examples-recipes/hr-leave-management.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"HR Leave Management Sample",sidebar_position:1,description:"Explore RCommon's HR Leave Management reference app demonstrating Clean Architecture, CQRS, EF Core, FluentValidation, JWT auth, and SendGrid email integration."},sidebar:"docsSidebar",previous:{title:"Examples & Recipes",permalink:"/docs/next/category/examples--recipes"},next:{title:"Event Handling Examples",permalink:"/docs/next/examples-recipes/event-handling"}},l={},d=[{value:"Sample Location",id:"sample-location",level:2},{value:"What the Sample Covers",id:"what-the-sample-covers",level:2},{value:"Domain Model",id:"domain-model",level:2},{value:"Entities",id:"entities",level:3},{value:"Domain Specifications",id:"domain-specifications",level:3},{value:"Application Layer",id:"application-layer",level:2},{value:"Commands and Queries",id:"commands-and-queries",level:3},{value:"Command Handler with Validation",id:"command-handler-with-validation",level:3},{value:"Command Handler with Multiple Repositories and Email",id:"command-handler-with-multiple-repositories-and-email",level:3},{value:"Allocation Command Handler",id:"allocation-command-handler",level:3},{value:"Validation",id:"validation",level:3},{value:"Persistence Layer",id:"persistence-layer",level:2},{value:"API Controllers",id:"api-controllers",level:2},{value:"Composition Root",id:"composition-root",level:2},{value:"Unit Testing Handlers",id:"unit-testing-handlers",level:2},{value:"Key Takeaways",id:"key-takeaways",level:2}];function c(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"hr-leave-management-sample",children:"HR Leave Management Sample"}),"\n",(0,t.jsx)(n.p,{children:"The HR Leave Management sample is the reference application included with RCommon. It demonstrates Clean Architecture, CQRS with MediatR, EF Core persistence, FluentValidation, JWT authentication, and SendGrid email \u2014 all wired together through RCommon's fluent builder. This walkthrough explains how each piece fits together."}),"\n",(0,t.jsx)(n.h2,{id:"sample-location",children:"Sample Location"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"Examples/CleanWithCQRS/\n HR.LeaveManagement.Domain/\n HR.LeaveManagement.Application/\n HR.LeaveManagement.Persistence/\n HR.LeaveManagement.Identity/\n HR.LeaveManagement.API/\n HR.LeaveManagement.MVC/\n HR.LeaveManagement.Application.UnitTests/\n"})}),"\n",(0,t.jsx)(n.h2,{id:"what-the-sample-covers",children:"What the Sample Covers"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"A leave management system where employees request time off"}),"\n",(0,t.jsx)(n.li,{children:"Administrators create leave types (e.g. Annual, Sick, Bereavement) and allocate days to employees"}),"\n",(0,t.jsx)(n.li,{children:"Employees submit leave requests against their allocations"}),"\n",(0,t.jsx)(n.li,{children:"Requests can be approved or rejected by administrators"}),"\n",(0,t.jsx)(n.li,{children:"Email confirmation is sent on successful request submission"}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"domain-model",children:"Domain Model"}),"\n",(0,t.jsx)(n.h3,{id:"entities",children:"Entities"}),"\n",(0,t.jsxs)(n.p,{children:["All entities inherit from ",(0,t.jsx)(n.code,{children:"BaseDomainEntity"}),", which inherits from RCommon's ",(0,t.jsx)(n.code,{children:"AuditedEntity"}),". This provides ",(0,t.jsx)(n.code,{children:"Id"}),", ",(0,t.jsx)(n.code,{children:"CreatedBy"}),", ",(0,t.jsx)(n.code,{children:"ModifiedBy"}),", ",(0,t.jsx)(n.code,{children:"CreateDate"}),", and ",(0,t.jsx)(n.code,{children:"ModifyDate"})," automatically:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'public abstract class BaseDomainEntity : AuditedEntity\n{\n}\n\npublic class LeaveType : BaseDomainEntity\n{\n public string Name { get; set; } // e.g. "Annual Leave"\n public int DefaultDays { get; set; } // e.g. 14\n}\n\npublic class LeaveAllocation : BaseDomainEntity\n{\n public int NumberOfDays { get; set; }\n public LeaveType LeaveType { get; set; }\n public int LeaveTypeId { get; set; }\n public int Period { get; set; } // year\n public string EmployeeId { get; set; }\n}\n\npublic class LeaveRequest : BaseDomainEntity\n{\n public DateTime StartDate { get; set; }\n public DateTime EndDate { get; set; }\n public LeaveType LeaveType { get; set; }\n public int LeaveTypeId { get; set; }\n public DateTime DateRequested { get; set; }\n public string RequestComments { get; set; }\n public bool? Approved { get; set; }\n public bool Cancelled { get; set; }\n public string RequestingEmployeeId { get; set; }\n}\n'})}),"\n",(0,t.jsx)(n.h3,{id:"domain-specifications",children:"Domain Specifications"}),"\n",(0,t.jsxs)(n.p,{children:["A specification encapsulates a reusable query predicate. The ",(0,t.jsx)(n.code,{children:"AllocationExistsSpec"})," checks whether an employee already has an allocation for a given leave type in a given year:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"public class AllocationExistsSpec : Specification\n{\n public AllocationExistsSpec(string userId, int leaveTypeId, int period)\n : base(q => q.EmployeeId == userId\n && q.LeaveTypeId == leaveTypeId\n && q.Period == period)\n {\n }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"Usage in a command handler:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"var allocationCount = await _leaveAllocationRepository\n .GetCountAsync(new AllocationExistsSpec(emp.Id, leaveType.Id, period));\n\nif (allocationCount > 0)\n continue; // already allocated for this period\n"})}),"\n",(0,t.jsx)(n.h2,{id:"application-layer",children:"Application Layer"}),"\n",(0,t.jsx)(n.h3,{id:"commands-and-queries",children:"Commands and Queries"}),"\n",(0,t.jsxs)(n.p,{children:["CQRS is expressed through RCommon's ",(0,t.jsx)(n.code,{children:"IAppRequest"})," and ",(0,t.jsx)(n.code,{children:"IAppRequestHandler"})," interfaces."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// Command \u2014 modifies state\npublic class CreateLeaveTypeCommand : IAppRequest\n{\n public CreateLeaveTypeDto LeaveTypeDto { get; set; }\n}\n\n// Query \u2014 reads state, no side effects\npublic class GetLeaveTypeListRequest : IAppRequest>\n{\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"command-handler-with-validation",children:"Command Handler with Validation"}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"CreateLeaveTypeCommandHandler"})," validates the incoming DTO via ",(0,t.jsx)(n.code,{children:"IValidationService"})," before persisting:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'public class CreateLeaveTypeCommandHandler\n : IAppRequestHandler\n{\n private readonly IGraphRepository _leaveTypeRepository;\n private readonly IValidationService _validationService;\n\n public CreateLeaveTypeCommandHandler(\n IGraphRepository leaveTypeRepository,\n IValidationService validationService)\n {\n _leaveTypeRepository = leaveTypeRepository;\n _validationService = validationService;\n _leaveTypeRepository.DataStoreName = DataStoreNamesConst.LeaveManagement;\n }\n\n public async Task HandleAsync(\n CreateLeaveTypeCommand request,\n CancellationToken cancellationToken)\n {\n var response = new BaseCommandResponse();\n var validationResult = await _validationService.ValidateAsync(request.LeaveTypeDto);\n\n if (!validationResult.IsValid)\n {\n response.Success = false;\n response.Message = "Creation Failed";\n response.Errors = validationResult.Errors\n .Select(q => q.ErrorMessage).ToList();\n }\n else\n {\n var leaveType = request.LeaveTypeDto.ToLeaveType();\n await _leaveTypeRepository.AddAsync(leaveType);\n response.Success = true;\n response.Message = "Creation Successful";\n response.Id = leaveType.Id;\n }\n\n return response;\n }\n}\n'})}),"\n",(0,t.jsx)(n.h3,{id:"command-handler-with-multiple-repositories-and-email",children:"Command Handler with Multiple Repositories and Email"}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"CreateLeaveRequestCommandHandler"})," coordinates multiple repositories, the current user identity, and email delivery. The handler remains unit-testable because all dependencies are injected as interfaces:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'public class CreateLeaveRequestCommandHandler\n : IAppRequestHandler\n{\n private readonly IReadOnlyRepository _leaveTypeRepository;\n private readonly IGraphRepository _leaveAllocationRepository;\n private readonly IGraphRepository _leaveRequestRepository;\n private readonly IEmailService _emailSender;\n private readonly ICurrentUser _currentUser;\n private readonly IValidationService _validationService;\n\n public async Task HandleAsync(\n CreateLeaveRequestCommand request,\n CancellationToken cancellationToken)\n {\n var response = new BaseCommandResponse();\n var validationResult = await _validationService.ValidateAsync(request.LeaveRequestDto);\n var userId = _currentUser.FindClaimValue(CustomClaimTypes.Uid);\n\n // Check allocation exists\n var allocation = _leaveAllocationRepository.FirstOrDefault(\n x => x.EmployeeId == userId\n && x.LeaveTypeId == request.LeaveRequestDto.LeaveTypeId);\n\n if (allocation is null)\n {\n validationResult.Errors.Add(new ValidationFault(\n nameof(request.LeaveRequestDto.LeaveTypeId),\n "You do not have any allocations for this leave type."));\n }\n else\n {\n int daysRequested = (int)(request.LeaveRequestDto.EndDate\n - request.LeaveRequestDto.StartDate).TotalDays;\n if (daysRequested > allocation.NumberOfDays)\n {\n validationResult.Errors.Add(new ValidationFault(\n nameof(request.LeaveRequestDto.EndDate),\n "You do not have enough days for this request"));\n }\n }\n\n if (!validationResult.IsValid)\n {\n response.Success = false;\n response.Message = "Request Failed";\n response.Errors = validationResult.Errors\n .Select(q => q.ErrorMessage).ToList();\n }\n else\n {\n var leaveRequest = request.LeaveRequestDto.ToLeaveRequest();\n leaveRequest.RequestingEmployeeId = userId;\n await _leaveRequestRepository.AddAsync(leaveRequest);\n\n response.Success = true;\n response.Message = "Request Created Successfully";\n response.Id = leaveRequest.Id;\n\n // Send confirmation email \u2014 failure does not roll back the request\n try\n {\n var emailAddress = _currentUser.FindClaimValue(ClaimTypes.Email);\n var email = new MailMessage(\n new MailAddress(fromEmail, fromName),\n new MailAddress(emailAddress))\n {\n Subject = "Leave Request Submitted",\n Body = $"Your leave request for {request.LeaveRequestDto.StartDate:D} " +\n $"to {request.LeaveRequestDto.EndDate:D} has been submitted."\n };\n await _emailSender.SendEmailAsync(email);\n }\n catch (Exception)\n {\n // Log but do not propagate \u2014 email is non-critical\n }\n }\n\n return response;\n }\n}\n'})}),"\n",(0,t.jsx)(n.h3,{id:"allocation-command-handler",children:"Allocation Command Handler"}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"CreateLeaveAllocationCommandHandler"})," demonstrates using a specification to avoid duplicate allocations. It retrieves all employees via the ",(0,t.jsx)(n.code,{children:"IUserService"})," contract and allocates days from the leave type's ",(0,t.jsx)(n.code,{children:"DefaultDays"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'public async Task HandleAsync(\n CreateLeaveAllocationCommand request,\n CancellationToken cancellationToken)\n{\n var leaveType = await _leaveTypeRepository\n .FindAsync(request.LeaveAllocationDto.LeaveTypeId);\n var employees = await _userService.GetEmployees();\n var period = DateTime.Now.Year;\n var allocations = new List();\n\n foreach (var emp in employees)\n {\n var allocationCount = await _leaveAllocationRepository\n .GetCountAsync(new AllocationExistsSpec(emp.Id, leaveType.Id, period));\n\n if (allocationCount > 0)\n continue;\n\n allocations.Add(new LeaveAllocation\n {\n EmployeeId = emp.Id,\n LeaveTypeId = leaveType.Id,\n NumberOfDays = leaveType.DefaultDays,\n Period = period\n });\n }\n\n foreach (var item in allocations)\n {\n await _leaveAllocationRepository.AddAsync(item);\n }\n\n return new BaseCommandResponse { Success = true, Message = "Allocations Successful" };\n}\n'})}),"\n",(0,t.jsx)(n.h3,{id:"validation",children:"Validation"}),"\n",(0,t.jsxs)(n.p,{children:["Validators use FluentValidation. They are discovered automatically via ",(0,t.jsx)(n.code,{children:"AddValidatorsFromAssemblyContaining"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'public class ILeaveTypeDtoValidator : AbstractValidator\n{\n public ILeaveTypeDtoValidator()\n {\n RuleFor(p => p.Name)\n .NotEmpty().WithMessage("{PropertyName} is required.")\n .NotNull()\n .MaximumLength(50)\n .WithMessage("{PropertyName} must not exceed {ComparisonValue} characters.");\n\n RuleFor(p => p.DefaultDays)\n .NotEmpty().WithMessage("{PropertyName} is required.")\n .GreaterThan(0).WithMessage("{PropertyName} must be at least 1.")\n .LessThan(100)\n .WithMessage("{PropertyName} must be less than {ComparisonValue}.");\n }\n}\n\npublic class CreateLeaveTypeDtoValidator : AbstractValidator\n{\n public CreateLeaveTypeDtoValidator()\n {\n Include(new ILeaveTypeDtoValidator());\n }\n}\n'})}),"\n",(0,t.jsx)(n.h2,{id:"persistence-layer",children:"Persistence Layer"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"LeaveManagementDbContext"})," inherits from ",(0,t.jsx)(n.code,{children:"AuditableDbContext"}),". RCommon injects ",(0,t.jsx)(n.code,{children:"ICurrentUser"})," and ",(0,t.jsx)(n.code,{children:"ISystemTime"})," to stamp audit fields on every ",(0,t.jsx)(n.code,{children:"SaveChanges"}),":"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"public class LeaveManagementDbContext : AuditableDbContext\n{\n public LeaveManagementDbContext(\n DbContextOptions options,\n ICurrentUser currentUser,\n ISystemTime systemTime)\n : base(options, currentUser, systemTime)\n {\n }\n\n protected override void OnModelCreating(ModelBuilder modelBuilder)\n {\n modelBuilder.ApplyConfigurationsFromAssembly(\n typeof(LeaveManagementDbContext).Assembly);\n }\n\n public DbSet LeaveRequests { get; set; }\n public DbSet LeaveTypes { get; set; }\n public DbSet LeaveAllocations { get; set; }\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"The data store name constant keeps the name in one place:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'public static class DataStoreNamesConst\n{\n public const string LeaveManagement = "LeaveManagement";\n}\n'})}),"\n",(0,t.jsx)(n.h2,{id:"api-controllers",children:"API Controllers"}),"\n",(0,t.jsxs)(n.p,{children:["Controllers use ",(0,t.jsx)(n.code,{children:"IMediatorService"})," exclusively \u2014 no direct handler references:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'[Route("api/[controller]")]\n[ApiController]\n[Authorize]\npublic class LeaveTypesController : ControllerBase\n{\n private readonly IMediatorService _mediator;\n\n public LeaveTypesController(IMediatorService mediator)\n => _mediator = mediator;\n\n [HttpGet]\n public async Task>> Get()\n {\n var leaveTypes = await _mediator.Send>(\n new GetLeaveTypeListRequest());\n return Ok(leaveTypes);\n }\n\n [HttpPost]\n [Authorize(Roles = "Administrator")]\n public async Task> Post(\n [FromBody] CreateLeaveTypeDto leaveType)\n {\n var command = new CreateLeaveTypeCommand { LeaveTypeDto = leaveType };\n var response = await _mediator.Send(command);\n return Ok(response);\n }\n\n [HttpPut("{id}")]\n [Authorize(Roles = "Administrator")]\n public async Task Put([FromBody] LeaveTypeDto leaveType)\n {\n var command = new UpdateLeaveTypeCommand { LeaveTypeDto = leaveType };\n await _mediator.Send(command);\n return NoContent();\n }\n\n [HttpDelete("{id}")]\n [Authorize(Roles = "Administrator")]\n public async Task Delete(int id)\n {\n var command = new DeleteLeaveTypeCommand { Id = id };\n await _mediator.Send(command);\n return NoContent();\n }\n}\n'})}),"\n",(0,t.jsx)(n.h2,{id:"composition-root",children:"Composition Root"}),"\n",(0,t.jsxs)(n.p,{children:["Everything is wired in ",(0,t.jsx)(n.code,{children:"Program.cs"}),". This is the only place in the system where concrete implementations are referenced by name:"]}),"\n",(0,t.jsx)(n.admonition,{title:"Modular composition",type:"tip",children:(0,t.jsxs)(n.p,{children:["The HR Leave Management sample keeps all wiring in a single ",(0,t.jsx)(n.code,{children:"Program.cs"})," for clarity, but RCommon also supports splitting this across modules. For example, an ",(0,t.jsx)(n.code,{children:"IdentityServicesRegistration.AddIdentityServices(services)"})," extension could call ",(0,t.jsx)(n.code,{children:"services.AddRCommon().WithClaimsAndPrincipalAccessor()"})," while a separate ",(0,t.jsx)(n.code,{children:"PersistenceServicesRegistration"})," calls ",(0,t.jsx)(n.code,{children:"services.AddRCommon().WithPersistence(...)"}),". The bootstrapper merges both into the same builder. See ",(0,t.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the contract."]})}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithClaimsAndPrincipalAccessor()\n .WithSendGridEmailServices(x =>\n {\n var settings = builder.Configuration.Get();\n x.SendGridApiKey = settings.SendGridApiKey;\n x.FromNameDefault = settings.FromNameDefault;\n x.FromEmailDefault = settings.FromEmailDefault;\n })\n .WithDateTimeSystem(dateTime => dateTime.Kind = DateTimeKind.Utc)\n .WithSequentialGuidGenerator(guid =>\n guid.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString)\n .WithMediator(mediator =>\n {\n mediator.AddRequest();\n mediator.AddRequest,\n GetLeaveTypeListRequestHandler>();\n // ... all other request/handler pairs\n\n mediator.Configure(config =>\n {\n config.RegisterServicesFromAssemblies(\n typeof(ApplicationServicesRegistration).GetTypeInfo().Assembly);\n });\n mediator.AddLoggingToRequestPipeline();\n mediator.AddUnitOfWorkToRequestPipeline();\n })\n .WithPersistence(ef =>\n {\n ef.AddDbContext(\n DataStoreNamesConst.LeaveManagement,\n options => options.UseSqlServer(\n builder.Configuration.GetConnectionString(\n DataStoreNamesConst.LeaveManagement)));\n ef.SetDefaultDataStore(dataStore =>\n dataStore.DefaultDataStoreName = DataStoreNamesConst.LeaveManagement);\n })\n .WithUnitOfWork(unitOfWork =>\n {\n unitOfWork.SetOptions(options =>\n {\n options.AutoCompleteScope = true;\n options.DefaultIsolation = IsolationLevel.ReadCommitted;\n });\n })\n .WithValidation(validation =>\n {\n validation.AddValidatorsFromAssemblyContaining(\n typeof(ApplicationServicesRegistration));\n });\n"})}),"\n",(0,t.jsx)(n.h2,{id:"unit-testing-handlers",children:"Unit Testing Handlers"}),"\n",(0,t.jsx)(n.p,{children:"Because every handler depends on interfaces, tests use mocks and require no database:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'[TestFixture]\npublic class CreateLeaveTypeCommandHandlerTests\n{\n private readonly CreateLeaveTypeDto _leaveTypeDto;\n private readonly CreateLeaveTypeCommandHandler _handler;\n\n public CreateLeaveTypeCommandHandlerTests()\n {\n var repositoryMock = new Mock>();\n var validationMock = new Mock();\n\n _leaveTypeDto = new CreateLeaveTypeDto\n {\n DefaultDays = 15,\n Name = "Test DTO"\n };\n\n validationMock\n .Setup(x => x.ValidateAsync(_leaveTypeDto, false, CancellationToken.None))\n .Returns(() => Task.FromResult(new ValidationOutcome()));\n\n _handler = new CreateLeaveTypeCommandHandler(\n repositoryMock.Object,\n validationMock.Object);\n }\n\n [Test]\n public async Task Valid_LeaveType_Added()\n {\n var result = await _handler.HandleAsync(\n new CreateLeaveTypeCommand { LeaveTypeDto = _leaveTypeDto },\n CancellationToken.None);\n\n result.ShouldBeOfType();\n result.Success.ShouldBeTrue();\n }\n}\n'})}),"\n",(0,t.jsx)(n.h2,{id:"key-takeaways",children:"Key Takeaways"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"IGraphRepository"})})," decouples handlers from the ORM. Switch from EF Core to NHibernate without touching a single handler."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"IValidationService"})})," decouples handlers from FluentValidation. The validator registration is a composition-root concern."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"ICurrentUser"})})," provides the authenticated user identity without importing ASP.NET Core into the domain or application layers."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"AuditableDbContext"})})," eliminates boilerplate audit stamping across every save."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Pipeline behaviors"})," (",(0,t.jsx)(n.code,{children:"AddLoggingToRequestPipeline"}),", ",(0,t.jsx)(n.code,{children:"AddUnitOfWorkToRequestPipeline"}),") apply cross-cutting concerns to every handler without modifying handler code."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:(0,t.jsx)(n.code,{children:"DataStoreName"})})," enables routing to the correct ",(0,t.jsx)(n.code,{children:"DbContext"})," when multiple databases are registered."]}),"\n"]})]})}function u(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(c,{...e})}):c(e)}},64996(e,n,a){a.d(n,{A:()=>r});a(30758);var t=a(13526);const i="tabItem_jA3u";var s=a(86070);function r({children:e,hidden:n,className:a}){return(0,s.jsx)("div",{role:"tabpanel",className:(0,t.A)(i,a),hidden:n,children:e})}},85363(e,n,a){a.d(n,{A:()=>R});var t=a(30758),i=a(13526),s=a(32416),r=a(25557),o=a(85924),l=a(64493),d=a(10274),c=a(44118);function u(e){return t.Children.toArray(e).filter(e=>"\n"!==e).map(e=>{if(!e||(0,t.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)})?.filter(Boolean)??[]}function p(e){const{values:n,children:a}=e;return(0,t.useMemo)(()=>{const e=n??function(e){return u(e).map(({props:{value:e,label:n,attributes:a,default:t}})=>({value:e,label:n,attributes:a,default:t}))}(a);return function(e){const n=(0,d.X)(e,(e,n)=>e.value===n.value);if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map(e=>e.value).join(", ")}" found in . Every value needs to be unique.`)}(e),e},[n,a])}function m({value:e,tabValues:n}){return n.some(n=>n.value===e)}function v({queryString:e=!1,groupId:n}){const a=(0,r.W6)(),i=function({queryString:e=!1,groupId:n}){if("string"==typeof e)return e;if(!1===e)return null;if(!0===e&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:e,groupId:n});return[(0,l.aZ)(i),(0,t.useCallback)(e=>{if(!i)return;const n=new URLSearchParams(a.location.search);n.set(i,e),a.replace({...a.location,search:n.toString()})},[i,a])]}function h(e){const{defaultValue:n,queryString:a=!1,groupId:i}=e,s=p(e),[r,l]=(0,t.useState)(()=>function({defaultValue:e,tabValues:n}){if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(e){if(!m({value:e,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${e}" but none of its children has the corresponding value. Available values are: ${n.map(e=>e.value).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return e}const a=n.find(e=>e.default)??n[0];if(!a)throw new Error("Unexpected error: 0 tabValues");return a.value}({defaultValue:n,tabValues:s})),[d,u]=v({queryString:a,groupId:i}),[h,y]=function({groupId:e}){const n=function(e){return e?`docusaurus.tab.${e}`:null}(e),[a,i]=(0,c.Dv)(n);return[a,(0,t.useCallback)(e=>{n&&i.set(e)},[n,i])]}({groupId:i}),g=(()=>{const e=d??h;return m({value:e,tabValues:s})?e:null})();(0,o.A)(()=>{g&&l(g)},[g]);return{selectedValue:r,selectValue:(0,t.useCallback)(e=>{if(!m({value:e,tabValues:s}))throw new Error(`Can't select invalid tab value=${e}`);l(e),u(e),y(e)},[u,y,s]),tabValues:s}}var y=a(81264);const g="tabList_TfjZ",x="tabItem_vVs9";var T=a(86070);function C({className:e,block:n,selectedValue:a,selectValue:t,tabValues:r}){const o=[],{blockElementScrollPositionUntilNextRender:l}=(0,s.a_)(),d=e=>{const n=e.currentTarget,i=o.indexOf(n),s=r[i].value;s!==a&&(l(n),t(s))},c=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const a=o.indexOf(e.currentTarget)+1;n=o[a]??o[0];break}case"ArrowLeft":{const a=o.indexOf(e.currentTarget)-1;n=o[a]??o[o.length-1];break}}n?.focus()};return(0,T.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,i.A)("tabs",{"tabs--block":n},e),children:r.map(({value:e,label:n,attributes:t})=>(0,T.jsx)("li",{role:"tab",tabIndex:a===e?0:-1,"aria-selected":a===e,ref:e=>o.push(e),onKeyDown:c,onClick:d,...t,className:(0,i.A)("tabs__item",x,t?.className,{"tabs__item--active":a===e}),children:n??e},e))})}function b({lazy:e,children:n,selectedValue:a}){const i=(Array.isArray(n)?n:[n]).filter(Boolean);if(e){const e=i.find(e=>e.props.value===a);return e?(0,t.cloneElement)(e,{className:"margin-top--md"}):null}return(0,T.jsx)("div",{className:"margin-top--md",children:i.map((e,n)=>(0,t.cloneElement)(e,{key:n,hidden:e.props.value!==a}))})}function f(e){const n=h(e);return(0,T.jsxs)("div",{className:(0,i.A)("tabs-container",g),children:[(0,T.jsx)(C,{...n,...e}),(0,T.jsx)(b,{...n,...e})]})}function R(e){const n=(0,y.A)();return(0,T.jsx)(f,{...e,children:u(e.children)},String(n))}},81753(e,n,a){a.d(n,{R:()=>r,x:()=>o});var t=a(30758);const i={},s=t.createContext(i);function r(e){const n=t.useContext(s);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:r(e.components),t.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/3cd55c21.12c791a5.js b/website/build/assets/js/3cd55c21.12c791a5.js deleted file mode 100644 index 57c7e418..00000000 --- a/website/build/assets/js/3cd55c21.12c791a5.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[5531],{32826(e,n,r){r.r(n),r.d(n,{assets:()=>o,contentTitle:()=>l,default:()=>h,frontMatter:()=>a,metadata:()=>d,toc:()=>c});var i=r(86070),s=r(81753),t=(r(85363),r(64996),r(75783));const a={title:"Messaging Examples",sidebar_position:4,description:"Messaging examples for RCommon showing MassTransit and Wolverine transports, subscription isolation between multiple builders, and in-process vs distributed routing."},l="Messaging Examples",d={id:"examples-recipes/messaging",title:"Messaging Examples",description:"Messaging examples for RCommon showing MassTransit and Wolverine transports, subscription isolation between multiple builders, and in-process vs distributed routing.",source:"@site/docs/examples-recipes/messaging.mdx",sourceDirName:"examples-recipes",slug:"/examples-recipes/messaging",permalink:"/docs/next/examples-recipes/messaging",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/examples-recipes/messaging.mdx",tags:[],version:"current",sidebarPosition:4,frontMatter:{title:"Messaging Examples",sidebar_position:4,description:"Messaging examples for RCommon showing MassTransit and Wolverine transports, subscription isolation between multiple builders, and in-process vs distributed routing."},sidebar:"docsSidebar",previous:{title:"Caching Examples",permalink:"/docs/next/examples-recipes/caching"},next:{title:"Testing",permalink:"/docs/next/category/testing"}},o={},c=[{value:"Example Projects",id:"example-projects",level:2},{value:"Step 1: Define an Event",id:"step-1-define-an-event",level:2},{value:"Step 2: Define a Subscriber",id:"step-2-define-a-subscriber",level:2},{value:"Example 1: MassTransit",id:"example-1-masstransit",level:2},{value:"Installation",id:"installation",level:3},{value:"Configuration",id:"configuration",level:3},{value:"Publishing via a Background Worker",id:"publishing-via-a-background-worker",level:3},{value:"Full Program.cs",id:"full-programcs",level:3},{value:"Example 2: Wolverine",id:"example-2-wolverine",level:2},{value:"Installation",id:"installation-1",level:3},{value:"Configuration",id:"configuration-1",level:3},{value:"Publishing",id:"publishing",level:3},{value:"Example 3: Subscription Isolation",id:"example-3-subscription-isolation",level:2},{value:"Scenario",id:"scenario",level:3},{value:"Event Definitions",id:"event-definitions",level:3},{value:"Configuration",id:"configuration-2",level:3},{value:"Publishing All Three Event Types",id:"publishing-all-three-event-types",level:3},{value:"SharedEventHandler",id:"sharedeventhandler",level:3},{value:"Expected Behavior",id:"expected-behavior",level:3},{value:"Comparing MassTransit and Wolverine",id:"comparing-masstransit-and-wolverine",level:2},{value:"Testing Messaging Code",id:"testing-messaging-code",level:2},{value:"API Reference",id:"api-reference",level:2}];function u(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"messaging-examples",children:"Messaging Examples"}),"\n",(0,i.jsx)(n.p,{children:"This page walks through the messaging examples included with RCommon. These examples show how to use MassTransit and Wolverine as the event transport, and how to isolate event subscriptions between multiple publishers."}),"\n",(0,i.jsx)(n.h2,{id:"example-projects",children:"Example Projects"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"Examples/Messaging/\r\n Examples.Messaging.MassTransit/ \u2014 MassTransit with in-memory transport\r\n Examples.Messaging.Wolverine/ \u2014 Wolverine with local queue\r\n Examples.Messaging.SubscriptionIsolation/ \u2014 Two builders with isolated subscriptions\n"})}),"\n",(0,i.jsx)(n.h2,{id:"step-1-define-an-event",children:"Step 1: Define an Event"}),"\n",(0,i.jsxs)(n.p,{children:["Events are plain classes that implement ",(0,i.jsx)(n.code,{children:"ISyncEvent"}),". They must have a default constructor so MassTransit or Wolverine can deserialize them off the wire:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Models.Events;\r\n\r\npublic class TestEvent : ISyncEvent\r\n{\r\n public TestEvent() { }\r\n\r\n public TestEvent(DateTime dateTime, Guid guid)\r\n {\r\n DateTime = dateTime;\r\n Guid = guid;\r\n }\r\n\r\n public DateTime DateTime { get; }\r\n public Guid Guid { get; }\r\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"step-2-define-a-subscriber",children:"Step 2: Define a Subscriber"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'using RCommon.EventHandling.Subscribers;\r\n\r\npublic class TestEventHandler : ISubscriber\r\n{\r\n public async Task HandleAsync(TestEvent notification,\r\n CancellationToken cancellationToken = default)\r\n {\r\n Console.WriteLine("I just handled this event {0}", notification.ToString());\r\n Console.WriteLine("Example Complete");\r\n await Task.CompletedTask;\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"example-1-masstransit",children:"Example 1: MassTransit"}),"\n",(0,i.jsx)(n.h3,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(t.A,{packageName:"RCommon.MassTransit"}),"\n",(0,i.jsx)(n.h3,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"services.AddRCommon()\r\n .WithEventHandling(eventHandling =>\r\n {\r\n // Use the in-memory transport for local development and testing\r\n eventHandling.UsingInMemory((context, cfg) =>\r\n {\r\n cfg.ConfigureEndpoints(context);\r\n });\r\n\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n });\n"})}),"\n",(0,i.jsxs)(n.p,{children:["For production, replace ",(0,i.jsx)(n.code,{children:"UsingInMemory"})," with ",(0,i.jsx)(n.code,{children:"UsingRabbitMq"}),", ",(0,i.jsx)(n.code,{children:"UsingAzureServiceBus"}),", or another MassTransit transport:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'eventHandling.UsingRabbitMq((context, cfg) =>\r\n{\r\n cfg.Host("rabbitmq://localhost");\r\n cfg.ConfigureEndpoints(context);\r\n});\n'})}),"\n",(0,i.jsx)(n.h3,{id:"publishing-via-a-background-worker",children:"Publishing via a Background Worker"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'public class Worker : BackgroundService\r\n{\r\n private readonly IServiceProvider _serviceProvider;\r\n private readonly IHostApplicationLifetime _lifetime;\r\n\r\n public Worker(IServiceProvider serviceProvider, IHostApplicationLifetime lifetime)\r\n {\r\n _serviceProvider = serviceProvider;\r\n _lifetime = lifetime;\r\n }\r\n\r\n protected override async Task ExecuteAsync(CancellationToken stoppingToken)\r\n {\r\n Console.WriteLine("Example Starting");\r\n\r\n var eventProducers = _serviceProvider.GetServices();\r\n var testEvent = new TestEvent(DateTime.Now, Guid.NewGuid());\r\n\r\n foreach (var producer in eventProducers)\r\n {\r\n Console.WriteLine($"Producer: {producer}");\r\n await producer.ProduceEventAsync(testEvent);\r\n }\r\n\r\n Console.WriteLine("Example Complete");\r\n _lifetime.StopApplication();\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(n.h3,{id:"full-programcs",children:"Full Program.cs"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using MassTransit;\r\nusing Microsoft.Extensions.DependencyInjection;\r\nusing Microsoft.Extensions.Hosting;\r\nusing RCommon;\r\nusing RCommon.EventHandling.Producers;\r\nusing RCommon.MassTransit;\r\nusing RCommon.MassTransit.Producers;\r\n\r\nvar host = Host.CreateDefaultBuilder(args)\r\n .ConfigureServices(services =>\r\n {\r\n services.AddRCommon()\r\n .WithEventHandling(eventHandling =>\r\n {\r\n eventHandling.UsingInMemory((context, cfg) =>\r\n {\r\n cfg.ConfigureEndpoints(context);\r\n });\r\n\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n });\r\n\r\n services.AddHostedService();\r\n }).Build();\r\n\r\nawait host.RunAsync();\n"})}),"\n",(0,i.jsx)(n.h2,{id:"example-2-wolverine",children:"Example 2: Wolverine"}),"\n",(0,i.jsx)(n.h3,{id:"installation-1",children:"Installation"}),"\n",(0,i.jsx)(t.A,{packageName:"RCommon.Wolverine"}),"\n",(0,i.jsxs)(n.p,{children:["Wolverine requires calling ",(0,i.jsx)(n.code,{children:"UseWolverine"})," on the host builder before configuring services:"]}),"\n",(0,i.jsx)(n.h3,{id:"configuration-1",children:"Configuration"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'using Wolverine;\r\nusing RCommon;\r\nusing RCommon.Wolverine;\r\nusing RCommon.Wolverine.Producers;\r\n\r\nvar host = Host.CreateDefaultBuilder(args)\r\n .UseWolverine(options =>\r\n {\r\n // Define a local queue for event processing\r\n options.LocalQueue("test");\r\n })\r\n .ConfigureServices(services =>\r\n {\r\n services.AddRCommon()\r\n .WithEventHandling(eventHandling =>\r\n {\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n });\r\n }).Build();\n'})}),"\n",(0,i.jsx)(n.h3,{id:"publishing",children:"Publishing"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"await host.StartAsync();\r\n\r\nvar eventProducers = host.Services.GetServices();\r\nvar testEvent = new TestEvent(DateTime.Now, Guid.NewGuid());\r\n\r\nforeach (var producer in eventProducers)\r\n{\r\n await producer.ProduceEventAsync(testEvent);\r\n}\n"})}),"\n",(0,i.jsx)(n.p,{children:"The pattern is identical to MassTransit. Only the builder type and the registered producer change."}),"\n",(0,i.jsx)(n.h2,{id:"example-3-subscription-isolation",children:"Example 3: Subscription Isolation"}),"\n",(0,i.jsx)(n.p,{children:"The subscription isolation example demonstrates that events are only routed to the producer/handler pair that subscribed them. This is critical in services with mixed internal and external messaging."}),"\n",(0,i.jsx)(n.h3,{id:"scenario",children:"Scenario"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"InMemoryOnlyEvent"})," \u2014 subscribed only in ",(0,i.jsx)(n.code,{children:"InMemoryEventBusBuilder"})]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"MassTransitOnlyEvent"})," \u2014 subscribed only in ",(0,i.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"})]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"SharedEvent"})," \u2014 subscribed in both builders, handled by both producer types"]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"event-definitions",children:"Event Definitions"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class InMemoryOnlyEvent : ISyncEvent\r\n{\r\n public InMemoryOnlyEvent() { }\r\n public InMemoryOnlyEvent(DateTime dateTime, Guid guid)\r\n {\r\n DateTime = dateTime;\r\n Guid = guid;\r\n }\r\n public DateTime DateTime { get; }\r\n public Guid Guid { get; }\r\n}\r\n\r\npublic class MassTransitOnlyEvent : ISyncEvent\r\n{\r\n public MassTransitOnlyEvent() { }\r\n public MassTransitOnlyEvent(DateTime dateTime, Guid guid)\r\n {\r\n DateTime = dateTime;\r\n Guid = guid;\r\n }\r\n public DateTime DateTime { get; }\r\n public Guid Guid { get; }\r\n}\r\n\r\npublic class SharedEvent : ISyncEvent\r\n{\r\n public SharedEvent() { }\r\n public SharedEvent(DateTime dateTime, Guid guid)\r\n {\r\n DateTime = dateTime;\r\n Guid = guid;\r\n }\r\n public DateTime DateTime { get; }\r\n public Guid Guid { get; }\r\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"configuration-2",children:"Configuration"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"services.AddRCommon()\r\n .WithEventHandling(eventHandling =>\r\n {\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n eventHandling.AddSubscriber();\r\n })\r\n .WithEventHandling(eventHandling =>\r\n {\r\n eventHandling.UsingInMemory((context, cfg) =>\r\n {\r\n cfg.ConfigureEndpoints(context);\r\n });\r\n\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n eventHandling.AddSubscriber();\r\n });\n"})}),"\n",(0,i.jsx)(n.h3,{id:"publishing-all-three-event-types",children:"Publishing All Three Event Types"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'protected override async Task ExecuteAsync(CancellationToken stoppingToken)\r\n{\r\n var eventProducers = _serviceProvider.GetServices();\r\n\r\n // Only InMemoryEventBus handles this \u2014 MassTransit producers ignore it\r\n var inMemoryEvent = new InMemoryOnlyEvent(DateTime.Now, Guid.NewGuid());\r\n Console.WriteLine("Publishing InMemoryOnlyEvent to all producers...");\r\n foreach (var producer in eventProducers)\r\n {\r\n Console.WriteLine($" -> Producer: {producer.GetType().Name}");\r\n await producer.ProduceEventAsync(inMemoryEvent);\r\n }\r\n\r\n // Only MassTransit handles this \u2014 in-memory producer ignores it\r\n var massTransitEvent = new MassTransitOnlyEvent(DateTime.Now, Guid.NewGuid());\r\n Console.WriteLine("Publishing MassTransitOnlyEvent to all producers...");\r\n foreach (var producer in eventProducers)\r\n {\r\n Console.WriteLine($" -> Producer: {producer.GetType().Name}");\r\n await producer.ProduceEventAsync(massTransitEvent);\r\n }\r\n\r\n // Both builders subscribed this \u2014 both producers handle it\r\n var sharedEvent = new SharedEvent(DateTime.Now, Guid.NewGuid());\r\n Console.WriteLine("Publishing SharedEvent to all producers (subscribed to both)...");\r\n foreach (var producer in eventProducers)\r\n {\r\n Console.WriteLine($" -> Producer: {producer.GetType().Name}");\r\n await producer.ProduceEventAsync(sharedEvent);\r\n }\r\n\r\n _lifetime.StopApplication();\r\n}\n'})}),"\n",(0,i.jsx)(n.h3,{id:"sharedeventhandler",children:"SharedEventHandler"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'public class SharedEventHandler : ISubscriber\r\n{\r\n public async Task HandleAsync(SharedEvent notification,\r\n CancellationToken cancellationToken = default)\r\n {\r\n Console.WriteLine("[SharedHandler] Handled SharedEvent: {0} | {1}",\r\n notification.DateTime, notification.Guid);\r\n await Task.CompletedTask;\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(n.h3,{id:"expected-behavior",children:"Expected Behavior"}),"\n",(0,i.jsxs)(n.p,{children:["When ",(0,i.jsx)(n.code,{children:"InMemoryOnlyEvent"})," is published through all producers:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"PublishWithEventBusEventProducer"})," routes it to ",(0,i.jsx)(n.code,{children:"InMemoryOnlyEventHandler"})," (subscribed)"]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})," ignores it (not subscribed in that builder)"]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["When ",(0,i.jsx)(n.code,{children:"SharedEvent"})," is published through all producers:"]}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"PublishWithEventBusEventProducer"})," routes it to ",(0,i.jsx)(n.code,{children:"SharedEventHandler"})]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})," also routes it to ",(0,i.jsx)(n.code,{children:"SharedEventHandler"})]}),"\n"]}),"\n",(0,i.jsx)(n.p,{children:"Both handlers fire independently."}),"\n",(0,i.jsx)(n.h2,{id:"comparing-masstransit-and-wolverine",children:"Comparing MassTransit and Wolverine"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Aspect"}),(0,i.jsx)(n.th,{children:"MassTransit"}),(0,i.jsx)(n.th,{children:"Wolverine"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Installation"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Wolverine"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Host setup"}),(0,i.jsxs)(n.td,{children:["Standard ",(0,i.jsx)(n.code,{children:"ConfigureServices"})]}),(0,i.jsxs)(n.td,{children:["Requires ",(0,i.jsx)(n.code,{children:"Host.UseWolverine(...)"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Transport options"}),(0,i.jsx)(n.td,{children:"RabbitMQ, Azure Service Bus, SQS, in-memory, and more"}),(0,i.jsx)(n.td,{children:"RabbitMQ, Azure Service Bus, in-memory local queues"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Saga / workflow support"}),(0,i.jsx)(n.td,{children:"Yes (via MassTransit state machines)"}),(0,i.jsx)(n.td,{children:"Yes (via Wolverine's saga support)"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Outbox support"}),(0,i.jsx)(n.td,{children:"Yes (via MassTransit Outbox)"}),(0,i.jsx)(n.td,{children:"Yes (built-in)"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"Local queue config"}),(0,i.jsx)(n.td,{children:"Automatic endpoint configuration"}),(0,i.jsxs)(n.td,{children:["Named local queues via ",(0,i.jsx)(n.code,{children:"options.LocalQueue(...)"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"RCommon builder"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:"RCommon producer"}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})})]})]})]}),"\n",(0,i.jsx)(n.h2,{id:"testing-messaging-code",children:"Testing Messaging Code"}),"\n",(0,i.jsx)(n.p,{children:"Event handlers are plain classes and do not require a running broker to test:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"[Test]\r\npublic async Task TestEventHandler_Completes_Without_Error()\r\n{\r\n var handler = new TestEventHandler();\r\n var @event = new TestEvent(DateTime.UtcNow, Guid.NewGuid());\r\n\r\n await handler.HandleAsync(@event, CancellationToken.None);\r\n // No exception = pass\r\n}\n"})}),"\n",(0,i.jsx)(n.p,{children:"For integration tests, use the MassTransit in-memory transport or Wolverine's test harness, which both run entirely in process."}),"\n",(0,i.jsx)(n.h2,{id:"api-reference",children:"API Reference"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Package"}),(0,i.jsx)(n.th,{children:"Purpose"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ISyncEvent"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Models"})}),(0,i.jsx)(n.td,{children:"Base interface for all events"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IEventProducer"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,i.jsx)(n.td,{children:"Publishes events"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ISubscriber"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,i.jsx)(n.td,{children:"Handles events"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,i.jsx)(n.td,{children:"MassTransit event handling configuration"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,i.jsx)(n.td,{children:"MassTransit-backed producer"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Wolverine"})}),(0,i.jsx)(n.td,{children:"Wolverine event handling configuration"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Wolverine"})}),(0,i.jsx)(n.td,{children:"Wolverine-backed producer"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InMemoryEventBusBuilder"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,i.jsx)(n.td,{children:"In-process event bus for isolation scenarios"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"PublishWithEventBusEventProducer"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,i.jsx)(n.td,{children:"In-memory producer"})]})]})]})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(u,{...e})}):u(e)}},64996(e,n,r){r.d(n,{A:()=>a});r(30758);var i=r(13526);const s="tabItem_jA3u";var t=r(86070);function a({children:e,hidden:n,className:r}){return(0,t.jsx)("div",{role:"tabpanel",className:(0,i.A)(s,r),hidden:n,children:e})}},85363(e,n,r){r.d(n,{A:()=>y});var i=r(30758),s=r(13526),t=r(32416),a=r(25557),l=r(85924),d=r(64493),o=r(10274),c=r(44118);function u(e){return i.Children.toArray(e).filter(e=>"\n"!==e).map(e=>{if(!e||(0,i.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)})?.filter(Boolean)??[]}function h(e){const{values:n,children:r}=e;return(0,i.useMemo)(()=>{const e=n??function(e){return u(e).map(({props:{value:e,label:n,attributes:r,default:i}})=>({value:e,label:n,attributes:r,default:i}))}(r);return function(e){const n=(0,o.X)(e,(e,n)=>e.value===n.value);if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map(e=>e.value).join(", ")}" found in . Every value needs to be unique.`)}(e),e},[n,r])}function v({value:e,tabValues:n}){return n.some(n=>n.value===e)}function p({queryString:e=!1,groupId:n}){const r=(0,a.W6)(),s=function({queryString:e=!1,groupId:n}){if("string"==typeof e)return e;if(!1===e)return null;if(!0===e&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:e,groupId:n});return[(0,d.aZ)(s),(0,i.useCallback)(e=>{if(!s)return;const n=new URLSearchParams(r.location.search);n.set(s,e),r.replace({...r.location,search:n.toString()})},[s,r])]}function m(e){const{defaultValue:n,queryString:r=!1,groupId:s}=e,t=h(e),[a,d]=(0,i.useState)(()=>function({defaultValue:e,tabValues:n}){if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(e){if(!v({value:e,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${e}" but none of its children has the corresponding value. Available values are: ${n.map(e=>e.value).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return e}const r=n.find(e=>e.default)??n[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:n,tabValues:t})),[o,u]=p({queryString:r,groupId:s}),[m,x]=function({groupId:e}){const n=function(e){return e?`docusaurus.tab.${e}`:null}(e),[r,s]=(0,c.Dv)(n);return[r,(0,i.useCallback)(e=>{n&&s.set(e)},[n,s])]}({groupId:s}),g=(()=>{const e=o??m;return v({value:e,tabValues:t})?e:null})();(0,l.A)(()=>{g&&d(g)},[g]);return{selectedValue:a,selectValue:(0,i.useCallback)(e=>{if(!v({value:e,tabValues:t}))throw new Error(`Can't select invalid tab value=${e}`);d(e),u(e),x(e)},[u,x,t]),tabValues:t}}var x=r(81264);const g="tabList_TfjZ",b="tabItem_vVs9";var j=r(86070);function E({className:e,block:n,selectedValue:r,selectValue:i,tabValues:a}){const l=[],{blockElementScrollPositionUntilNextRender:d}=(0,t.a_)(),o=e=>{const n=e.currentTarget,s=l.indexOf(n),t=a[s].value;t!==r&&(d(n),i(t))},c=e=>{let n=null;switch(e.key){case"Enter":o(e);break;case"ArrowRight":{const r=l.indexOf(e.currentTarget)+1;n=l[r]??l[0];break}case"ArrowLeft":{const r=l.indexOf(e.currentTarget)-1;n=l[r]??l[l.length-1];break}}n?.focus()};return(0,j.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,s.A)("tabs",{"tabs--block":n},e),children:a.map(({value:e,label:n,attributes:i})=>(0,j.jsx)("li",{role:"tab",tabIndex:r===e?0:-1,"aria-selected":r===e,ref:e=>l.push(e),onKeyDown:c,onClick:o,...i,className:(0,s.A)("tabs__item",b,i?.className,{"tabs__item--active":r===e}),children:n??e},e))})}function f({lazy:e,children:n,selectedValue:r}){const s=(Array.isArray(n)?n:[n]).filter(Boolean);if(e){const e=s.find(e=>e.props.value===r);return e?(0,i.cloneElement)(e,{className:"margin-top--md"}):null}return(0,j.jsx)("div",{className:"margin-top--md",children:s.map((e,n)=>(0,i.cloneElement)(e,{key:n,hidden:e.props.value!==r}))})}function T(e){const n=m(e);return(0,j.jsxs)("div",{className:(0,s.A)("tabs-container",g),children:[(0,j.jsx)(E,{...n,...e}),(0,j.jsx)(f,{...n,...e})]})}function y(e){const n=(0,x.A)();return(0,j.jsx)(T,{...e,children:u(e.children)},String(n))}},75783(e,n,r){r.d(n,{A:()=>c});var i=r(30758);const s="container_xjrG",t="label_Y4p8",a="commandRow_FY5I",l="command_m7Qs",d="copyButton_u1GK";var o=r(86070);function c({packageName:e,version:n}){const[r,c]=(0,i.useState)(!1),u=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,o.jsxs)("div",{className:s,children:[(0,o.jsx)("div",{className:t,children:"NuGet Package"}),(0,o.jsxs)("div",{className:a,children:[(0,o.jsx)("code",{className:l,children:u}),(0,o.jsx)("button",{className:d,onClick:()=>{navigator.clipboard.writeText(u),c(!0),setTimeout(()=>c(!1),2e3)},title:"Copy to clipboard",children:r?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,r){r.d(n,{R:()=>a,x:()=>l});var i=r(30758);const s={},t=i.createContext(s);function a(e){const n=i.useContext(t);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),i.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/3cd55c21.8f2d82e8.js b/website/build/assets/js/3cd55c21.8f2d82e8.js new file mode 100644 index 00000000..e19fca92 --- /dev/null +++ b/website/build/assets/js/3cd55c21.8f2d82e8.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[5531],{32826(e,n,i){i.r(n),i.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>h,frontMatter:()=>a,metadata:()=>o,toc:()=>c});var s=i(86070),r=i(81753),t=(i(85363),i(64996),i(75783));const a={title:"Messaging Examples",sidebar_position:4,description:"Messaging examples for RCommon showing MassTransit and Wolverine transports, subscription isolation between multiple builders, and in-process vs distributed routing."},l="Messaging Examples",o={id:"examples-recipes/messaging",title:"Messaging Examples",description:"Messaging examples for RCommon showing MassTransit and Wolverine transports, subscription isolation between multiple builders, and in-process vs distributed routing.",source:"@site/docs/examples-recipes/messaging.mdx",sourceDirName:"examples-recipes",slug:"/examples-recipes/messaging",permalink:"/docs/next/examples-recipes/messaging",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/examples-recipes/messaging.mdx",tags:[],version:"current",sidebarPosition:4,frontMatter:{title:"Messaging Examples",sidebar_position:4,description:"Messaging examples for RCommon showing MassTransit and Wolverine transports, subscription isolation between multiple builders, and in-process vs distributed routing."},sidebar:"docsSidebar",previous:{title:"Caching Examples",permalink:"/docs/next/examples-recipes/caching"},next:{title:"Testing",permalink:"/docs/next/category/testing"}},d={},c=[{value:"Example Projects",id:"example-projects",level:2},{value:"Step 1: Define an Event",id:"step-1-define-an-event",level:2},{value:"Step 2: Define a Subscriber",id:"step-2-define-a-subscriber",level:2},{value:"Example 1: MassTransit",id:"example-1-masstransit",level:2},{value:"Installation",id:"installation",level:3},{value:"Configuration",id:"configuration",level:3},{value:"Publishing via a Background Worker",id:"publishing-via-a-background-worker",level:3},{value:"Full Program.cs",id:"full-programcs",level:3},{value:"Example 2: Wolverine",id:"example-2-wolverine",level:2},{value:"Installation",id:"installation-1",level:3},{value:"Configuration",id:"configuration-1",level:3},{value:"Publishing",id:"publishing",level:3},{value:"Example 3: Subscription Isolation",id:"example-3-subscription-isolation",level:2},{value:"Scenario",id:"scenario",level:3},{value:"Event Definitions",id:"event-definitions",level:3},{value:"Configuration",id:"configuration-2",level:3},{value:"Publishing All Three Event Types",id:"publishing-all-three-event-types",level:3},{value:"SharedEventHandler",id:"sharedeventhandler",level:3},{value:"Expected Behavior",id:"expected-behavior",level:3},{value:"Comparing MassTransit and Wolverine",id:"comparing-masstransit-and-wolverine",level:2},{value:"Testing Messaging Code",id:"testing-messaging-code",level:2},{value:"API Reference",id:"api-reference",level:2}];function u(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"messaging-examples",children:"Messaging Examples"}),"\n",(0,s.jsx)(n.p,{children:"This page walks through the messaging examples included with RCommon. These examples show how to use MassTransit and Wolverine as the event transport, and how to isolate event subscriptions between multiple publishers."}),"\n",(0,s.jsx)(n.h2,{id:"example-projects",children:"Example Projects"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{children:"Examples/Messaging/\n Examples.Messaging.MassTransit/ \u2014 MassTransit with in-memory transport\n Examples.Messaging.Wolverine/ \u2014 Wolverine with local queue\n Examples.Messaging.SubscriptionIsolation/ \u2014 Two builders with isolated subscriptions\n"})}),"\n",(0,s.jsx)(n.h2,{id:"step-1-define-an-event",children:"Step 1: Define an Event"}),"\n",(0,s.jsxs)(n.p,{children:["Events are plain classes that implement ",(0,s.jsx)(n.code,{children:"ISyncEvent"}),". They must have a default constructor so MassTransit or Wolverine can deserialize them off the wire:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Models.Events;\n\npublic class TestEvent : ISyncEvent\n{\n public TestEvent() { }\n\n public TestEvent(DateTime dateTime, Guid guid)\n {\n DateTime = dateTime;\n Guid = guid;\n }\n\n public DateTime DateTime { get; }\n public Guid Guid { get; }\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"step-2-define-a-subscriber",children:"Step 2: Define a Subscriber"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'using RCommon.EventHandling.Subscribers;\n\npublic class TestEventHandler : ISubscriber\n{\n public async Task HandleAsync(TestEvent notification,\n CancellationToken cancellationToken = default)\n {\n Console.WriteLine("I just handled this event {0}", notification.ToString());\n Console.WriteLine("Example Complete");\n await Task.CompletedTask;\n }\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"example-1-masstransit",children:"Example 1: MassTransit"}),"\n",(0,s.jsx)(n.h3,{id:"installation",children:"Installation"}),"\n",(0,s.jsx)(t.A,{packageName:"RCommon.MassTransit"}),"\n",(0,s.jsx)(n.h3,{id:"configuration",children:"Configuration"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"services.AddRCommon()\n .WithEventHandling(eventHandling =>\n {\n // Use the in-memory transport for local development and testing\n eventHandling.UsingInMemory((context, cfg) =>\n {\n cfg.ConfigureEndpoints(context);\n });\n\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n });\n"})}),"\n",(0,s.jsxs)(n.p,{children:["For production, replace ",(0,s.jsx)(n.code,{children:"UsingInMemory"})," with ",(0,s.jsx)(n.code,{children:"UsingRabbitMq"}),", ",(0,s.jsx)(n.code,{children:"UsingAzureServiceBus"}),", or another MassTransit transport:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'eventHandling.UsingRabbitMq((context, cfg) =>\n{\n cfg.Host("rabbitmq://localhost");\n cfg.ConfigureEndpoints(context);\n});\n'})}),"\n",(0,s.jsx)(n.h3,{id:"publishing-via-a-background-worker",children:"Publishing via a Background Worker"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'public class Worker : BackgroundService\n{\n private readonly IServiceProvider _serviceProvider;\n private readonly IHostApplicationLifetime _lifetime;\n\n public Worker(IServiceProvider serviceProvider, IHostApplicationLifetime lifetime)\n {\n _serviceProvider = serviceProvider;\n _lifetime = lifetime;\n }\n\n protected override async Task ExecuteAsync(CancellationToken stoppingToken)\n {\n Console.WriteLine("Example Starting");\n\n var eventProducers = _serviceProvider.GetServices();\n var testEvent = new TestEvent(DateTime.Now, Guid.NewGuid());\n\n foreach (var producer in eventProducers)\n {\n Console.WriteLine($"Producer: {producer}");\n await producer.ProduceEventAsync(testEvent);\n }\n\n Console.WriteLine("Example Complete");\n _lifetime.StopApplication();\n }\n}\n'})}),"\n",(0,s.jsx)(n.h3,{id:"full-programcs",children:"Full Program.cs"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"using MassTransit;\nusing Microsoft.Extensions.DependencyInjection;\nusing Microsoft.Extensions.Hosting;\nusing RCommon;\nusing RCommon.EventHandling.Producers;\nusing RCommon.MassTransit;\nusing RCommon.MassTransit.Producers;\n\nvar host = Host.CreateDefaultBuilder(args)\n .ConfigureServices(services =>\n {\n services.AddRCommon()\n .WithEventHandling(eventHandling =>\n {\n eventHandling.UsingInMemory((context, cfg) =>\n {\n cfg.ConfigureEndpoints(context);\n });\n\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n });\n\n services.AddHostedService();\n }).Build();\n\nawait host.RunAsync();\n"})}),"\n",(0,s.jsx)(n.h2,{id:"example-2-wolverine",children:"Example 2: Wolverine"}),"\n",(0,s.jsx)(n.h3,{id:"installation-1",children:"Installation"}),"\n",(0,s.jsx)(t.A,{packageName:"RCommon.Wolverine"}),"\n",(0,s.jsxs)(n.p,{children:["Wolverine requires calling ",(0,s.jsx)(n.code,{children:"UseWolverine"})," on the host builder before configuring services:"]}),"\n",(0,s.jsx)(n.h3,{id:"configuration-1",children:"Configuration"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'using Wolverine;\nusing RCommon;\nusing RCommon.Wolverine;\nusing RCommon.Wolverine.Producers;\n\nvar host = Host.CreateDefaultBuilder(args)\n .UseWolverine(options =>\n {\n // Define a local queue for event processing\n options.LocalQueue("test");\n })\n .ConfigureServices(services =>\n {\n services.AddRCommon()\n .WithEventHandling(eventHandling =>\n {\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n });\n }).Build();\n'})}),"\n",(0,s.jsx)(n.h3,{id:"publishing",children:"Publishing"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"await host.StartAsync();\n\nvar eventProducers = host.Services.GetServices();\nvar testEvent = new TestEvent(DateTime.Now, Guid.NewGuid());\n\nforeach (var producer in eventProducers)\n{\n await producer.ProduceEventAsync(testEvent);\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:"The pattern is identical to MassTransit. Only the builder type and the registered producer change."}),"\n",(0,s.jsx)(n.h2,{id:"example-3-subscription-isolation",children:"Example 3: Subscription Isolation"}),"\n",(0,s.jsx)(n.p,{children:"The subscription isolation example demonstrates that events are only routed to the producer/handler pair that subscribed them. This is critical in services with mixed internal and external messaging."}),"\n",(0,s.jsx)(n.admonition,{title:"Modular composition",type:"tip",children:(0,s.jsxs)(n.p,{children:["The two-builder pattern shown below works whether you wire it in one ",(0,s.jsx)(n.code,{children:"Program.cs"})," or split it across modules. Each module can call ",(0,s.jsx)(n.code,{children:"services.AddRCommon().WithEventHandling(...)"})," independently \u2014 the cached builder is reused and subscriber registrations accumulate. The MassTransit builder has a known cross-module limitation documented on the ",(0,s.jsx)(n.a,{href:"/docs/next/event-handling/masstransit#configuration",children:"MassTransit event handling page"}),"; see ",(0,s.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full contract."]})}),"\n",(0,s.jsx)(n.h3,{id:"scenario",children:"Scenario"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"InMemoryOnlyEvent"})," \u2014 subscribed only in ",(0,s.jsx)(n.code,{children:"InMemoryEventBusBuilder"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"MassTransitOnlyEvent"})," \u2014 subscribed only in ",(0,s.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"SharedEvent"})," \u2014 subscribed in both builders, handled by both producer types"]}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"event-definitions",children:"Event Definitions"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public class InMemoryOnlyEvent : ISyncEvent\n{\n public InMemoryOnlyEvent() { }\n public InMemoryOnlyEvent(DateTime dateTime, Guid guid)\n {\n DateTime = dateTime;\n Guid = guid;\n }\n public DateTime DateTime { get; }\n public Guid Guid { get; }\n}\n\npublic class MassTransitOnlyEvent : ISyncEvent\n{\n public MassTransitOnlyEvent() { }\n public MassTransitOnlyEvent(DateTime dateTime, Guid guid)\n {\n DateTime = dateTime;\n Guid = guid;\n }\n public DateTime DateTime { get; }\n public Guid Guid { get; }\n}\n\npublic class SharedEvent : ISyncEvent\n{\n public SharedEvent() { }\n public SharedEvent(DateTime dateTime, Guid guid)\n {\n DateTime = dateTime;\n Guid = guid;\n }\n public DateTime DateTime { get; }\n public Guid Guid { get; }\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"configuration-2",children:"Configuration"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"services.AddRCommon()\n .WithEventHandling(eventHandling =>\n {\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n eventHandling.AddSubscriber();\n })\n .WithEventHandling(eventHandling =>\n {\n eventHandling.UsingInMemory((context, cfg) =>\n {\n cfg.ConfigureEndpoints(context);\n });\n\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n eventHandling.AddSubscriber();\n });\n"})}),"\n",(0,s.jsx)(n.h3,{id:"publishing-all-three-event-types",children:"Publishing All Three Event Types"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'protected override async Task ExecuteAsync(CancellationToken stoppingToken)\n{\n var eventProducers = _serviceProvider.GetServices();\n\n // Only InMemoryEventBus handles this \u2014 MassTransit producers ignore it\n var inMemoryEvent = new InMemoryOnlyEvent(DateTime.Now, Guid.NewGuid());\n Console.WriteLine("Publishing InMemoryOnlyEvent to all producers...");\n foreach (var producer in eventProducers)\n {\n Console.WriteLine($" -> Producer: {producer.GetType().Name}");\n await producer.ProduceEventAsync(inMemoryEvent);\n }\n\n // Only MassTransit handles this \u2014 in-memory producer ignores it\n var massTransitEvent = new MassTransitOnlyEvent(DateTime.Now, Guid.NewGuid());\n Console.WriteLine("Publishing MassTransitOnlyEvent to all producers...");\n foreach (var producer in eventProducers)\n {\n Console.WriteLine($" -> Producer: {producer.GetType().Name}");\n await producer.ProduceEventAsync(massTransitEvent);\n }\n\n // Both builders subscribed this \u2014 both producers handle it\n var sharedEvent = new SharedEvent(DateTime.Now, Guid.NewGuid());\n Console.WriteLine("Publishing SharedEvent to all producers (subscribed to both)...");\n foreach (var producer in eventProducers)\n {\n Console.WriteLine($" -> Producer: {producer.GetType().Name}");\n await producer.ProduceEventAsync(sharedEvent);\n }\n\n _lifetime.StopApplication();\n}\n'})}),"\n",(0,s.jsx)(n.h3,{id:"sharedeventhandler",children:"SharedEventHandler"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'public class SharedEventHandler : ISubscriber\n{\n public async Task HandleAsync(SharedEvent notification,\n CancellationToken cancellationToken = default)\n {\n Console.WriteLine("[SharedHandler] Handled SharedEvent: {0} | {1}",\n notification.DateTime, notification.Guid);\n await Task.CompletedTask;\n }\n}\n'})}),"\n",(0,s.jsx)(n.h3,{id:"expected-behavior",children:"Expected Behavior"}),"\n",(0,s.jsxs)(n.p,{children:["When ",(0,s.jsx)(n.code,{children:"InMemoryOnlyEvent"})," is published through all producers:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"PublishWithEventBusEventProducer"})," routes it to ",(0,s.jsx)(n.code,{children:"InMemoryOnlyEventHandler"})," (subscribed)"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})," ignores it (not subscribed in that builder)"]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["When ",(0,s.jsx)(n.code,{children:"SharedEvent"})," is published through all producers:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"PublishWithEventBusEventProducer"})," routes it to ",(0,s.jsx)(n.code,{children:"SharedEventHandler"})]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})," also routes it to ",(0,s.jsx)(n.code,{children:"SharedEventHandler"})]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"Both handlers fire independently."}),"\n",(0,s.jsx)(n.h2,{id:"comparing-masstransit-and-wolverine",children:"Comparing MassTransit and Wolverine"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Aspect"}),(0,s.jsx)(n.th,{children:"MassTransit"}),(0,s.jsx)(n.th,{children:"Wolverine"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Installation"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.Wolverine"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Host setup"}),(0,s.jsxs)(n.td,{children:["Standard ",(0,s.jsx)(n.code,{children:"ConfigureServices"})]}),(0,s.jsxs)(n.td,{children:["Requires ",(0,s.jsx)(n.code,{children:"Host.UseWolverine(...)"})]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Transport options"}),(0,s.jsx)(n.td,{children:"RabbitMQ, Azure Service Bus, SQS, in-memory, and more"}),(0,s.jsx)(n.td,{children:"RabbitMQ, Azure Service Bus, in-memory local queues"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Saga / workflow support"}),(0,s.jsx)(n.td,{children:"Yes (via MassTransit state machines)"}),(0,s.jsx)(n.td,{children:"Yes (via Wolverine's saga support)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Outbox support"}),(0,s.jsx)(n.td,{children:"Yes (via MassTransit Outbox)"}),(0,s.jsx)(n.td,{children:"Yes (built-in)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Local queue config"}),(0,s.jsx)(n.td,{children:"Automatic endpoint configuration"}),(0,s.jsxs)(n.td,{children:["Named local queues via ",(0,s.jsx)(n.code,{children:"options.LocalQueue(...)"})]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"RCommon builder"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"RCommon producer"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:"testing-messaging-code",children:"Testing Messaging Code"}),"\n",(0,s.jsx)(n.p,{children:"Event handlers are plain classes and do not require a running broker to test:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"[Test]\npublic async Task TestEventHandler_Completes_Without_Error()\n{\n var handler = new TestEventHandler();\n var @event = new TestEvent(DateTime.UtcNow, Guid.NewGuid());\n\n await handler.HandleAsync(@event, CancellationToken.None);\n // No exception = pass\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:"For integration tests, use the MassTransit in-memory transport or Wolverine's test harness, which both run entirely in process."}),"\n",(0,s.jsx)(n.h2,{id:"api-reference",children:"API Reference"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Package"}),(0,s.jsx)(n.th,{children:"Purpose"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"ISyncEvent"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.Models"})}),(0,s.jsx)(n.td,{children:"Base interface for all events"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"IEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(n.td,{children:"Publishes events"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"ISubscriber"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(n.td,{children:"Handles events"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,s.jsx)(n.td,{children:"MassTransit event handling configuration"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,s.jsx)(n.td,{children:"MassTransit-backed producer"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.Wolverine"})}),(0,s.jsx)(n.td,{children:"Wolverine event handling configuration"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.Wolverine"})}),(0,s.jsx)(n.td,{children:"Wolverine-backed producer"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"InMemoryEventBusBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(n.td,{children:"In-process event bus for isolation scenarios"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithEventBusEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(n.td,{children:"In-memory producer"})]})]})]})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(u,{...e})}):u(e)}},64996(e,n,i){i.d(n,{A:()=>a});i(30758);var s=i(13526);const r="tabItem_jA3u";var t=i(86070);function a({children:e,hidden:n,className:i}){return(0,t.jsx)("div",{role:"tabpanel",className:(0,s.A)(r,i),hidden:n,children:e})}},85363(e,n,i){i.d(n,{A:()=>y});var s=i(30758),r=i(13526),t=i(32416),a=i(25557),l=i(85924),o=i(64493),d=i(10274),c=i(44118);function u(e){return s.Children.toArray(e).filter(e=>"\n"!==e).map(e=>{if(!e||(0,s.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)})?.filter(Boolean)??[]}function h(e){const{values:n,children:i}=e;return(0,s.useMemo)(()=>{const e=n??function(e){return u(e).map(({props:{value:e,label:n,attributes:i,default:s}})=>({value:e,label:n,attributes:i,default:s}))}(i);return function(e){const n=(0,d.X)(e,(e,n)=>e.value===n.value);if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map(e=>e.value).join(", ")}" found in . Every value needs to be unique.`)}(e),e},[n,i])}function v({value:e,tabValues:n}){return n.some(n=>n.value===e)}function p({queryString:e=!1,groupId:n}){const i=(0,a.W6)(),r=function({queryString:e=!1,groupId:n}){if("string"==typeof e)return e;if(!1===e)return null;if(!0===e&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:e,groupId:n});return[(0,o.aZ)(r),(0,s.useCallback)(e=>{if(!r)return;const n=new URLSearchParams(i.location.search);n.set(r,e),i.replace({...i.location,search:n.toString()})},[r,i])]}function m(e){const{defaultValue:n,queryString:i=!1,groupId:r}=e,t=h(e),[a,o]=(0,s.useState)(()=>function({defaultValue:e,tabValues:n}){if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(e){if(!v({value:e,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${e}" but none of its children has the corresponding value. Available values are: ${n.map(e=>e.value).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return e}const i=n.find(e=>e.default)??n[0];if(!i)throw new Error("Unexpected error: 0 tabValues");return i.value}({defaultValue:n,tabValues:t})),[d,u]=p({queryString:i,groupId:r}),[m,x]=function({groupId:e}){const n=function(e){return e?`docusaurus.tab.${e}`:null}(e),[i,r]=(0,c.Dv)(n);return[i,(0,s.useCallback)(e=>{n&&r.set(e)},[n,r])]}({groupId:r}),g=(()=>{const e=d??m;return v({value:e,tabValues:t})?e:null})();(0,l.A)(()=>{g&&o(g)},[g]);return{selectedValue:a,selectValue:(0,s.useCallback)(e=>{if(!v({value:e,tabValues:t}))throw new Error(`Can't select invalid tab value=${e}`);o(e),u(e),x(e)},[u,x,t]),tabValues:t}}var x=i(81264);const g="tabList_TfjZ",b="tabItem_vVs9";var j=i(86070);function E({className:e,block:n,selectedValue:i,selectValue:s,tabValues:a}){const l=[],{blockElementScrollPositionUntilNextRender:o}=(0,t.a_)(),d=e=>{const n=e.currentTarget,r=l.indexOf(n),t=a[r].value;t!==i&&(o(n),s(t))},c=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const i=l.indexOf(e.currentTarget)+1;n=l[i]??l[0];break}case"ArrowLeft":{const i=l.indexOf(e.currentTarget)-1;n=l[i]??l[l.length-1];break}}n?.focus()};return(0,j.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,r.A)("tabs",{"tabs--block":n},e),children:a.map(({value:e,label:n,attributes:s})=>(0,j.jsx)("li",{role:"tab",tabIndex:i===e?0:-1,"aria-selected":i===e,ref:e=>l.push(e),onKeyDown:c,onClick:d,...s,className:(0,r.A)("tabs__item",b,s?.className,{"tabs__item--active":i===e}),children:n??e},e))})}function f({lazy:e,children:n,selectedValue:i}){const r=(Array.isArray(n)?n:[n]).filter(Boolean);if(e){const e=r.find(e=>e.props.value===i);return e?(0,s.cloneElement)(e,{className:"margin-top--md"}):null}return(0,j.jsx)("div",{className:"margin-top--md",children:r.map((e,n)=>(0,s.cloneElement)(e,{key:n,hidden:e.props.value!==i}))})}function T(e){const n=m(e);return(0,j.jsxs)("div",{className:(0,r.A)("tabs-container",g),children:[(0,j.jsx)(E,{...n,...e}),(0,j.jsx)(f,{...n,...e})]})}function y(e){const n=(0,x.A)();return(0,j.jsx)(T,{...e,children:u(e.children)},String(n))}},75783(e,n,i){i.d(n,{A:()=>c});var s=i(30758);const r="container_xjrG",t="label_Y4p8",a="commandRow_FY5I",l="command_m7Qs",o="copyButton_u1GK";var d=i(86070);function c({packageName:e,version:n}){const[i,c]=(0,s.useState)(!1),u=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,d.jsxs)("div",{className:r,children:[(0,d.jsx)("div",{className:t,children:"NuGet Package"}),(0,d.jsxs)("div",{className:a,children:[(0,d.jsx)("code",{className:l,children:u}),(0,d.jsx)("button",{className:o,onClick:()=>{navigator.clipboard.writeText(u),c(!0),setTimeout(()=>c(!1),2e3)},title:"Copy to clipboard",children:i?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,i){i.d(n,{R:()=>a,x:()=>l});var s=i(30758);const r={},t=s.createContext(r);function a(e){const n=s.useContext(t);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),s.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/3df138ae.69c963cb.js b/website/build/assets/js/3df138ae.69c963cb.js new file mode 100644 index 00000000..300aba5b --- /dev/null +++ b/website/build/assets/js/3df138ae.69c963cb.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[4994],{49103(e,i,n){n.r(i),n.d(i,{assets:()=>o,contentTitle:()=>t,default:()=>l,frontMatter:()=>s,metadata:()=>a,toc:()=>d});var c=n(86070),r=n(81753);const s={title:"Overview",sidebar_position:1,description:"Provider-agnostic caching abstraction for .NET with ICacheService, strongly-typed CacheKey, and support for in-memory, distributed, and Redis backends."},t="Caching Overview",a={id:"caching/overview",title:"Overview",description:"Provider-agnostic caching abstraction for .NET with ICacheService, strongly-typed CacheKey, and support for in-memory, distributed, and Redis backends.",source:"@site/docs/caching/overview.mdx",sourceDirName:"caching",slug:"/caching/overview",permalink:"/docs/next/caching/overview",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/caching/overview.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Overview",sidebar_position:1,description:"Provider-agnostic caching abstraction for .NET with ICacheService, strongly-typed CacheKey, and support for in-memory, distributed, and Redis backends."},sidebar:"docsSidebar",previous:{title:"Caching",permalink:"/docs/next/category/caching"},next:{title:"Memory Cache",permalink:"/docs/next/caching/memory"}},o={},d=[{value:"ICacheService",id:"icacheservice",level:2},{value:"CacheKey",id:"cachekey",level:2},{value:"When to use caching",id:"when-to-use-caching",level:2},{value:"Expression caching",id:"expression-caching",level:2},{value:"Choosing a caching provider",id:"choosing-a-caching-provider",level:2},{value:"Builder hierarchy",id:"builder-hierarchy",level:2},{value:"Section contents",id:"section-contents",level:2}];function h(e){const i={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)(i.h1,{id:"caching-overview",children:"Caching Overview"}),"\n",(0,c.jsxs)(i.p,{children:["RCommon provides a provider-agnostic caching abstraction built around a single interface: ",(0,c.jsx)(i.code,{children:"ICacheService"}),". The same interface is implemented by in-process memory caching, distributed memory caching, and Redis. Application code that depends only on ",(0,c.jsx)(i.code,{children:"ICacheService"})," can switch caching backends without any handler or service changes."]}),"\n",(0,c.jsx)(i.h2,{id:"icacheservice",children:"ICacheService"}),"\n",(0,c.jsxs)(i.p,{children:[(0,c.jsx)(i.code,{children:"ICacheService"})," exposes a get-or-create pattern. If the requested key exists in the cache the cached value is returned immediately. If the key is absent the factory delegate is invoked, the result is stored in the cache, and the value is returned."]}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:"public interface ICacheService\n{\n TData GetOrCreate(object key, Func data);\n Task GetOrCreateAsync(object key, Func data);\n}\n"})}),"\n",(0,c.jsxs)(i.p,{children:["All implementations \u2014 ",(0,c.jsx)(i.code,{children:"InMemoryCacheService"}),", ",(0,c.jsx)(i.code,{children:"DistributedMemoryCacheService"}),", and ",(0,c.jsx)(i.code,{children:"RedisCacheService"})," \u2014 implement this interface. The provider is swapped by changing the builder call in startup; consuming code does not change."]}),"\n",(0,c.jsx)(i.h2,{id:"cachekey",children:"CacheKey"}),"\n",(0,c.jsxs)(i.p,{children:[(0,c.jsx)(i.code,{children:"CacheKey"})," is a strongly-typed wrapper around a cache key string. It validates that the key is non-empty and does not exceed 256 characters. Use the factory methods to build composite keys:"]}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:'using RCommon.Caching;\n\n// Simple key from string parts\nvar key = CacheKey.With("orders", orderId.ToString());\n\n// Scoped to a type\nvar key = CacheKey.With(typeof(OrderService), "list", tenantId.ToString());\n// Produces: "OrderService:list-"\n'})}),"\n",(0,c.jsxs)(i.p,{children:["Passing a ",(0,c.jsx)(i.code,{children:"CacheKey"})," as the ",(0,c.jsx)(i.code,{children:"key"})," argument to ",(0,c.jsx)(i.code,{children:"ICacheService"})," methods ensures consistent key formatting across the application."]}),"\n",(0,c.jsx)(i.h2,{id:"when-to-use-caching",children:"When to use caching"}),"\n",(0,c.jsxs)(i.p,{children:["Use ",(0,c.jsx)(i.code,{children:"ICacheService"})," when:"]}),"\n",(0,c.jsxs)(i.ul,{children:["\n",(0,c.jsx)(i.li,{children:"A query or computation is expensive and the result changes infrequently (reference data, configuration, aggregated reports)."}),"\n",(0,c.jsx)(i.li,{children:"You want to protect a downstream service or database from repeated identical requests."}),"\n",(0,c.jsx)(i.li,{children:"You are caching dynamically compiled expressions or reflection results to improve performance (RCommon uses this internally for repository expression caching)."}),"\n"]}),"\n",(0,c.jsx)(i.p,{children:"Do not use caching when:"}),"\n",(0,c.jsxs)(i.ul,{children:["\n",(0,c.jsx)(i.li,{children:"The data must always be fresh and stale reads are unacceptable."}),"\n",(0,c.jsx)(i.li,{children:"The data is user-specific and not shared across requests (unless you scope the cache key per user)."}),"\n",(0,c.jsx)(i.li,{children:"The cached value is trivially cheap to compute."}),"\n"]}),"\n",(0,c.jsx)(i.h2,{id:"expression-caching",children:"Expression caching"}),"\n",(0,c.jsxs)(i.p,{children:["RCommon uses caching internally to compile and cache LINQ expression trees that would otherwise be recompiled on every repository call. Enabling ",(0,c.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions"})," on either the in-memory or Redis builder activates this optimization:"]}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithMemoryCaching(cache =>\n {\n cache.CacheDynamicallyCompiledExpressions();\n });\n"})}),"\n",(0,c.jsx)(i.p,{children:"This is the recommended minimum caching configuration for applications that use the persistence layer."}),"\n",(0,c.jsx)(i.h2,{id:"choosing-a-caching-provider",children:"Choosing a caching provider"}),"\n",(0,c.jsxs)(i.table,{children:[(0,c.jsx)(i.thead,{children:(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.th,{children:"Provider"}),(0,c.jsx)(i.th,{children:"Package"}),(0,c.jsx)(i.th,{children:"Backing store"}),(0,c.jsx)(i.th,{children:"Cross-process"})]})}),(0,c.jsxs)(i.tbody,{children:[(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:"In-memory"}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"IMemoryCache"})}),(0,c.jsx)(i.td,{children:"No"})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:"Distributed memory"}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,c.jsxs)(i.td,{children:[(0,c.jsx)(i.code,{children:"IDistributedCache"})," (in-process)"]}),(0,c.jsx)(i.td,{children:"No"})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:"Redis"}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.RedisCache"})}),(0,c.jsx)(i.td,{children:"StackExchange.Redis"}),(0,c.jsx)(i.td,{children:"Yes"})]})]})]}),"\n",(0,c.jsx)(i.p,{children:"Use the in-memory provider for single-process applications or development environments. Use Redis when you need a cache that is shared across multiple instances of the same service."}),"\n",(0,c.jsx)(i.h2,{id:"builder-hierarchy",children:"Builder hierarchy"}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{children:"IRCommonBuilder\n .WithMemoryCaching() \u2192 IMemoryCachingBuilder\n .WithDistributedCaching() \u2192 IDistributedCachingBuilder\n"})}),"\n",(0,c.jsxs)(i.p,{children:[(0,c.jsx)(i.code,{children:"IMemoryCachingBuilder"})," and ",(0,c.jsx)(i.code,{children:"IDistributedCachingBuilder"})," are marker interfaces. Provider packages implement them with concrete builder types (",(0,c.jsx)(i.code,{children:"InMemoryCachingBuilder"}),", ",(0,c.jsx)(i.code,{children:"DistributedMemoryCacheBuilder"}),", ",(0,c.jsx)(i.code,{children:"RedisCachingBuilder"}),") that expose provider-specific extension methods."]}),"\n",(0,c.jsx)(i.admonition,{title:"Modular composition",type:"tip",children:(0,c.jsxs)(i.p,{children:["Both ",(0,c.jsx)(i.code,{children:"WithMemoryCaching"})," and ",(0,c.jsx)(i.code,{children:"WithDistributedCaching"})," are cache-aware sub-builder verbs. When two modules call ",(0,c.jsx)(i.code,{children:"WithMemoryCaching"})," (or ",(0,c.jsx)(i.code,{children:"WithDistributedCaching"}),") with the same concrete builder type, the cached sub-builder is reused and each module's configuration delegate runs against the same instance \u2014 ",(0,c.jsx)(i.code,{children:"Configure(...)"})," and ",(0,c.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions()"})," registrations accumulate. Different concrete builder types coexist side by side (e.g. ",(0,c.jsx)(i.code,{children:"WithMemoryCaching"})," and ",(0,c.jsx)(i.code,{children:"WithDistributedCaching"})," register independently). See ",(0,c.jsx)(i.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,c.jsx)(i.h2,{id:"section-contents",children:"Section contents"}),"\n",(0,c.jsxs)(i.ul,{children:["\n",(0,c.jsxs)(i.li,{children:[(0,c.jsx)(i.a,{href:"/docs/next/caching/memory",children:"Memory Cache"})," \u2014 In-process memory caching setup, configuration options, eviction, and expression caching"]}),"\n",(0,c.jsxs)(i.li,{children:[(0,c.jsx)(i.a,{href:"/docs/next/caching/redis",children:"Redis Cache"})," \u2014 Redis setup, connection string configuration, distributed caching, and expression caching"]}),"\n"]})]})}function l(e={}){const{wrapper:i}={...(0,r.R)(),...e.components};return i?(0,c.jsx)(i,{...e,children:(0,c.jsx)(h,{...e})}):h(e)}},81753(e,i,n){n.d(i,{R:()=>t,x:()=>a});var c=n(30758);const r={},s=c.createContext(r);function t(e){const i=c.useContext(s);return c.useMemo(function(){return"function"==typeof e?e(i):{...i,...e}},[i,e])}function a(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:t(e.components),c.createElement(s.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/3df138ae.7d21539c.js b/website/build/assets/js/3df138ae.7d21539c.js deleted file mode 100644 index 122da551..00000000 --- a/website/build/assets/js/3df138ae.7d21539c.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[4994],{49103(e,i,n){n.r(i),n.d(i,{assets:()=>o,contentTitle:()=>t,default:()=>l,frontMatter:()=>s,metadata:()=>a,toc:()=>d});var c=n(86070),r=n(81753);const s={title:"Overview",sidebar_position:1,description:"Provider-agnostic caching abstraction for .NET with ICacheService, strongly-typed CacheKey, and support for in-memory, distributed, and Redis backends."},t="Caching Overview",a={id:"caching/overview",title:"Overview",description:"Provider-agnostic caching abstraction for .NET with ICacheService, strongly-typed CacheKey, and support for in-memory, distributed, and Redis backends.",source:"@site/docs/caching/overview.mdx",sourceDirName:"caching",slug:"/caching/overview",permalink:"/docs/next/caching/overview",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/caching/overview.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Overview",sidebar_position:1,description:"Provider-agnostic caching abstraction for .NET with ICacheService, strongly-typed CacheKey, and support for in-memory, distributed, and Redis backends."},sidebar:"docsSidebar",previous:{title:"Caching",permalink:"/docs/next/category/caching"},next:{title:"Memory Cache",permalink:"/docs/next/caching/memory"}},o={},d=[{value:"ICacheService",id:"icacheservice",level:2},{value:"CacheKey",id:"cachekey",level:2},{value:"When to use caching",id:"when-to-use-caching",level:2},{value:"Expression caching",id:"expression-caching",level:2},{value:"Choosing a caching provider",id:"choosing-a-caching-provider",level:2},{value:"Builder hierarchy",id:"builder-hierarchy",level:2},{value:"Section contents",id:"section-contents",level:2}];function h(e){const i={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)(i.h1,{id:"caching-overview",children:"Caching Overview"}),"\n",(0,c.jsxs)(i.p,{children:["RCommon provides a provider-agnostic caching abstraction built around a single interface: ",(0,c.jsx)(i.code,{children:"ICacheService"}),". The same interface is implemented by in-process memory caching, distributed memory caching, and Redis. Application code that depends only on ",(0,c.jsx)(i.code,{children:"ICacheService"})," can switch caching backends without any handler or service changes."]}),"\n",(0,c.jsx)(i.h2,{id:"icacheservice",children:"ICacheService"}),"\n",(0,c.jsxs)(i.p,{children:[(0,c.jsx)(i.code,{children:"ICacheService"})," exposes a get-or-create pattern. If the requested key exists in the cache the cached value is returned immediately. If the key is absent the factory delegate is invoked, the result is stored in the cache, and the value is returned."]}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:"public interface ICacheService\r\n{\r\n TData GetOrCreate(object key, Func data);\r\n Task GetOrCreateAsync(object key, Func data);\r\n}\n"})}),"\n",(0,c.jsxs)(i.p,{children:["All implementations \u2014 ",(0,c.jsx)(i.code,{children:"InMemoryCacheService"}),", ",(0,c.jsx)(i.code,{children:"DistributedMemoryCacheService"}),", and ",(0,c.jsx)(i.code,{children:"RedisCacheService"})," \u2014 implement this interface. The provider is swapped by changing the builder call in startup; consuming code does not change."]}),"\n",(0,c.jsx)(i.h2,{id:"cachekey",children:"CacheKey"}),"\n",(0,c.jsxs)(i.p,{children:[(0,c.jsx)(i.code,{children:"CacheKey"})," is a strongly-typed wrapper around a cache key string. It validates that the key is non-empty and does not exceed 256 characters. Use the factory methods to build composite keys:"]}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:'using RCommon.Caching;\r\n\r\n// Simple key from string parts\r\nvar key = CacheKey.With("orders", orderId.ToString());\r\n\r\n// Scoped to a type\r\nvar key = CacheKey.With(typeof(OrderService), "list", tenantId.ToString());\r\n// Produces: "OrderService:list-"\n'})}),"\n",(0,c.jsxs)(i.p,{children:["Passing a ",(0,c.jsx)(i.code,{children:"CacheKey"})," as the ",(0,c.jsx)(i.code,{children:"key"})," argument to ",(0,c.jsx)(i.code,{children:"ICacheService"})," methods ensures consistent key formatting across the application."]}),"\n",(0,c.jsx)(i.h2,{id:"when-to-use-caching",children:"When to use caching"}),"\n",(0,c.jsxs)(i.p,{children:["Use ",(0,c.jsx)(i.code,{children:"ICacheService"})," when:"]}),"\n",(0,c.jsxs)(i.ul,{children:["\n",(0,c.jsx)(i.li,{children:"A query or computation is expensive and the result changes infrequently (reference data, configuration, aggregated reports)."}),"\n",(0,c.jsx)(i.li,{children:"You want to protect a downstream service or database from repeated identical requests."}),"\n",(0,c.jsx)(i.li,{children:"You are caching dynamically compiled expressions or reflection results to improve performance (RCommon uses this internally for repository expression caching)."}),"\n"]}),"\n",(0,c.jsx)(i.p,{children:"Do not use caching when:"}),"\n",(0,c.jsxs)(i.ul,{children:["\n",(0,c.jsx)(i.li,{children:"The data must always be fresh and stale reads are unacceptable."}),"\n",(0,c.jsx)(i.li,{children:"The data is user-specific and not shared across requests (unless you scope the cache key per user)."}),"\n",(0,c.jsx)(i.li,{children:"The cached value is trivially cheap to compute."}),"\n"]}),"\n",(0,c.jsx)(i.h2,{id:"expression-caching",children:"Expression caching"}),"\n",(0,c.jsxs)(i.p,{children:["RCommon uses caching internally to compile and cache LINQ expression trees that would otherwise be recompiled on every repository call. Enabling ",(0,c.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions"})," on either the in-memory or Redis builder activates this optimization:"]}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithMemoryCaching(cache =>\r\n {\r\n cache.CacheDynamicallyCompiledExpressions();\r\n });\n"})}),"\n",(0,c.jsx)(i.p,{children:"This is the recommended minimum caching configuration for applications that use the persistence layer."}),"\n",(0,c.jsx)(i.h2,{id:"choosing-a-caching-provider",children:"Choosing a caching provider"}),"\n",(0,c.jsxs)(i.table,{children:[(0,c.jsx)(i.thead,{children:(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.th,{children:"Provider"}),(0,c.jsx)(i.th,{children:"Package"}),(0,c.jsx)(i.th,{children:"Backing store"}),(0,c.jsx)(i.th,{children:"Cross-process"})]})}),(0,c.jsxs)(i.tbody,{children:[(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:"In-memory"}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"IMemoryCache"})}),(0,c.jsx)(i.td,{children:"No"})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:"Distributed memory"}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.MemoryCache"})}),(0,c.jsxs)(i.td,{children:[(0,c.jsx)(i.code,{children:"IDistributedCache"})," (in-process)"]}),(0,c.jsx)(i.td,{children:"No"})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:"Redis"}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.RedisCache"})}),(0,c.jsx)(i.td,{children:"StackExchange.Redis"}),(0,c.jsx)(i.td,{children:"Yes"})]})]})]}),"\n",(0,c.jsx)(i.p,{children:"Use the in-memory provider for single-process applications or development environments. Use Redis when you need a cache that is shared across multiple instances of the same service."}),"\n",(0,c.jsx)(i.h2,{id:"builder-hierarchy",children:"Builder hierarchy"}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{children:"IRCommonBuilder\r\n .WithMemoryCaching() \u2192 IMemoryCachingBuilder\r\n .WithDistributedCaching() \u2192 IDistributedCachingBuilder\n"})}),"\n",(0,c.jsxs)(i.p,{children:[(0,c.jsx)(i.code,{children:"IMemoryCachingBuilder"})," and ",(0,c.jsx)(i.code,{children:"IDistributedCachingBuilder"})," are marker interfaces. Provider packages implement them with concrete builder types (",(0,c.jsx)(i.code,{children:"InMemoryCachingBuilder"}),", ",(0,c.jsx)(i.code,{children:"DistributedMemoryCacheBuilder"}),", ",(0,c.jsx)(i.code,{children:"RedisCachingBuilder"}),") that expose provider-specific extension methods."]}),"\n",(0,c.jsx)(i.h2,{id:"section-contents",children:"Section contents"}),"\n",(0,c.jsxs)(i.ul,{children:["\n",(0,c.jsxs)(i.li,{children:[(0,c.jsx)(i.a,{href:"/docs/next/caching/memory",children:"Memory Cache"})," \u2014 In-process memory caching setup, configuration options, eviction, and expression caching"]}),"\n",(0,c.jsxs)(i.li,{children:[(0,c.jsx)(i.a,{href:"/docs/next/caching/redis",children:"Redis Cache"})," \u2014 Redis setup, connection string configuration, distributed caching, and expression caching"]}),"\n"]})]})}function l(e={}){const{wrapper:i}={...(0,r.R)(),...e.components};return i?(0,c.jsx)(i,{...e,children:(0,c.jsx)(h,{...e})}):h(e)}},81753(e,i,n){n.d(i,{R:()=>t,x:()=>a});var c=n(30758);const r={},s=c.createContext(r);function t(e){const i=c.useContext(s);return c.useMemo(function(){return"function"==typeof e?e(i):{...i,...e}},[i,e])}function a(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:t(e.components),c.createElement(s.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/462845a6.04e309d6.js b/website/build/assets/js/462845a6.04e309d6.js deleted file mode 100644 index 5ec95ffb..00000000 --- a/website/build/assets/js/462845a6.04e309d6.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[5999],{42722(e,r,n){n.r(r),n.d(r,{assets:()=>c,contentTitle:()=>o,default:()=>u,frontMatter:()=>i,metadata:()=>a,toc:()=>d});var s=n(86070),t=n(81753);n(85363),n(64996);const i={title:"Microservices",sidebar_position:2,description:"Build .NET microservices with RCommon using MassTransit or Wolverine for messaging, per-service persistence, and transport-agnostic IEventProducer abstractions."},o="Microservices with RCommon",a={id:"architecture-guides/microservices",title:"Microservices",description:"Build .NET microservices with RCommon using MassTransit or Wolverine for messaging, per-service persistence, and transport-agnostic IEventProducer abstractions.",source:"@site/docs/architecture-guides/microservices.mdx",sourceDirName:"architecture-guides",slug:"/architecture-guides/microservices",permalink:"/docs/next/architecture-guides/microservices",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/architecture-guides/microservices.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Microservices",sidebar_position:2,description:"Build .NET microservices with RCommon using MassTransit or Wolverine for messaging, per-service persistence, and transport-agnostic IEventProducer abstractions."},sidebar:"docsSidebar",previous:{title:"Clean Architecture",permalink:"/docs/next/architecture-guides/clean-architecture"},next:{title:"Event-Driven Architecture",permalink:"/docs/next/architecture-guides/event-driven"}},c={},d=[{value:"When to Use This Approach",id:"when-to-use-this-approach",level:2},{value:"Core Patterns for Microservices",id:"core-patterns-for-microservices",level:2},{value:"Setting Up a Service with Messaging",id:"setting-up-a-service-with-messaging",level:2},{value:"Defining Events as Shared Contracts",id:"defining-events-as-shared-contracts",level:2},{value:"Publishing Events",id:"publishing-events",level:2},{value:"Subscribing to Events",id:"subscribing-to-events",level:2},{value:"Subscription Isolation",id:"subscription-isolation",level:2},{value:"Wolverine Alternative",id:"wolverine-alternative",level:2},{value:"Sharing Infrastructure Abstractions Across Services",id:"sharing-infrastructure-abstractions-across-services",level:2},{value:"Per-Service Persistence",id:"per-service-persistence",level:2},{value:"Health Considerations",id:"health-considerations",level:2},{value:"API Reference",id:"api-reference",level:2}];function l(e){const r={code:"code",h1:"h1",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(r.h1,{id:"microservices-with-rcommon",children:"Microservices with RCommon"}),"\n",(0,s.jsx)(r.p,{children:"Microservices decompose a system into independently deployable services that communicate over a network. RCommon provides the abstractions and integrations that make this communication consistent, testable, and swappable across a distributed system."}),"\n",(0,s.jsx)(r.h2,{id:"when-to-use-this-approach",children:"When to Use This Approach"}),"\n",(0,s.jsx)(r.p,{children:"Consider a microservices architecture with RCommon when:"}),"\n",(0,s.jsxs)(r.ul,{children:["\n",(0,s.jsx)(r.li,{children:"Different parts of your domain have different scaling requirements"}),"\n",(0,s.jsx)(r.li,{children:"Separate teams own separate services and need clear contracts between them"}),"\n",(0,s.jsx)(r.li,{children:"You need to evolve service internals without redeploying the entire system"}),"\n",(0,s.jsx)(r.li,{children:"Integration points require durable, reliable messaging (not just HTTP calls)"}),"\n"]}),"\n",(0,s.jsx)(r.h2,{id:"core-patterns-for-microservices",children:"Core Patterns for Microservices"}),"\n",(0,s.jsx)(r.p,{children:"RCommon supports three primary patterns that are essential in microservice environments:"}),"\n",(0,s.jsxs)(r.ol,{children:["\n",(0,s.jsxs)(r.li,{children:[(0,s.jsx)(r.strong,{children:"Messaging"})," \u2014 Publish events that other services subscribe to via MassTransit or Wolverine"]}),"\n",(0,s.jsxs)(r.li,{children:[(0,s.jsx)(r.strong,{children:"Shared abstractions"})," \u2014 ",(0,s.jsx)(r.code,{children:"IGraphRepository"}),", ",(0,s.jsx)(r.code,{children:"IMediatorService"}),", and ",(0,s.jsx)(r.code,{children:"IEventProducer"})," are the same across every service; only the registered implementation changes"]}),"\n",(0,s.jsxs)(r.li,{children:[(0,s.jsx)(r.strong,{children:"Subscription isolation"})," \u2014 Each service subscribes to exactly the events it needs, routing them to the correct transport"]}),"\n"]}),"\n",(0,s.jsx)(r.h2,{id:"setting-up-a-service-with-messaging",children:"Setting Up a Service with Messaging"}),"\n",(0,s.jsxs)(r.p,{children:["Each microservice calls ",(0,s.jsx)(r.code,{children:"AddRCommon()"})," independently. The following shows how a service is configured to publish and receive messages via MassTransit:"]}),"\n",(0,s.jsx)(r.pre,{children:(0,s.jsx)(r.code,{className:"language-csharp",children:'// Program.cs \u2014 Order Service\r\nservices.AddRCommon()\r\n .WithEventHandling(eventHandling =>\r\n {\r\n // Configure the transport (RabbitMQ, Azure Service Bus, etc.)\r\n eventHandling.UsingRabbitMq((context, cfg) =>\r\n {\r\n cfg.Host("rabbitmq://localhost");\r\n cfg.ConfigureEndpoints(context);\r\n });\r\n\r\n // This service produces order events\r\n eventHandling.AddProducer();\r\n\r\n // This service consumes inventory events\r\n eventHandling.AddSubscriber();\r\n });\n'})}),"\n",(0,s.jsx)(r.p,{children:"On the inventory service side:"}),"\n",(0,s.jsx)(r.pre,{children:(0,s.jsx)(r.code,{className:"language-csharp",children:'// Program.cs \u2014 Inventory Service\r\nservices.AddRCommon()\r\n .WithEventHandling(eventHandling =>\r\n {\r\n eventHandling.UsingRabbitMq((context, cfg) =>\r\n {\r\n cfg.Host("rabbitmq://localhost");\r\n cfg.ConfigureEndpoints(context);\r\n });\r\n\r\n eventHandling.AddProducer();\r\n\r\n // This service handles order placed events\r\n eventHandling.AddSubscriber();\r\n });\n'})}),"\n",(0,s.jsx)(r.h2,{id:"defining-events-as-shared-contracts",children:"Defining Events as Shared Contracts"}),"\n",(0,s.jsx)(r.p,{children:"Events that cross service boundaries should be defined in a shared contracts library. Both services reference this library, never each other:"}),"\n",(0,s.jsx)(r.pre,{children:(0,s.jsx)(r.code,{className:"language-csharp",children:"// SharedContracts/OrderPlacedEvent.cs\r\nusing RCommon.Models.Events;\r\n\r\npublic class OrderPlacedEvent : ISyncEvent\r\n{\r\n public OrderPlacedEvent() { }\r\n\r\n public OrderPlacedEvent(Guid orderId, string customerId, decimal total)\r\n {\r\n OrderId = orderId;\r\n CustomerId = customerId;\r\n Total = total;\r\n }\r\n\r\n public Guid OrderId { get; }\r\n public string CustomerId { get; }\r\n public decimal Total { get; }\r\n}\n"})}),"\n",(0,s.jsxs)(r.p,{children:[(0,s.jsx)(r.code,{children:"ISyncEvent"})," is RCommon's base marker interface. The transport-specific serialization and routing is handled by the infrastructure layer \u2014 the event itself stays clean."]}),"\n",(0,s.jsx)(r.h2,{id:"publishing-events",children:"Publishing Events"}),"\n",(0,s.jsxs)(r.p,{children:["Services publish events through ",(0,s.jsx)(r.code,{children:"IEventProducer"}),". The call site does not know which transport is registered:"]}),"\n",(0,s.jsx)(r.pre,{children:(0,s.jsx)(r.code,{className:"language-csharp",children:"public class PlaceOrderCommandHandler\r\n : IAppRequestHandler\r\n{\r\n private readonly IGraphRepository _orderRepository;\r\n private readonly IEnumerable _eventProducers;\r\n\r\n public PlaceOrderCommandHandler(\r\n IGraphRepository orderRepository,\r\n IEnumerable eventProducers)\r\n {\r\n _orderRepository = orderRepository;\r\n _eventProducers = eventProducers;\r\n }\r\n\r\n public async Task HandleAsync(\r\n PlaceOrderCommand request,\r\n CancellationToken cancellationToken)\r\n {\r\n var order = new Order(request.CustomerId, request.Items);\r\n await _orderRepository.AddAsync(order);\r\n\r\n var @event = new OrderPlacedEvent(order.Id, order.CustomerId, order.Total);\r\n\r\n foreach (var producer in _eventProducers)\r\n {\r\n await producer.ProduceEventAsync(@event);\r\n }\r\n\r\n return new BaseCommandResponse { Success = true, Id = order.Id };\r\n }\r\n}\n"})}),"\n",(0,s.jsx)(r.h2,{id:"subscribing-to-events",children:"Subscribing to Events"}),"\n",(0,s.jsxs)(r.p,{children:["Event handlers implement ",(0,s.jsx)(r.code,{children:"ISubscriber"}),". The handler contains only business logic \u2014 no transport concerns:"]}),"\n",(0,s.jsx)(r.pre,{children:(0,s.jsx)(r.code,{className:"language-csharp",children:"// Inventory Service\r\npublic class ReserveInventoryHandler : ISubscriber\r\n{\r\n private readonly IGraphRepository _inventoryRepository;\r\n private readonly IEnumerable _eventProducers;\r\n\r\n public ReserveInventoryHandler(\r\n IGraphRepository inventoryRepository,\r\n IEnumerable eventProducers)\r\n {\r\n _inventoryRepository = inventoryRepository;\r\n _eventProducers = eventProducers;\r\n }\r\n\r\n public async Task HandleAsync(\r\n OrderPlacedEvent notification,\r\n CancellationToken cancellationToken = default)\r\n {\r\n // Reserve inventory items\r\n var reserved = await ReserveItems(notification.OrderId);\r\n\r\n // Emit a confirmation event back\r\n var confirmEvent = new InventoryReservedEvent(notification.OrderId, reserved);\r\n foreach (var producer in _eventProducers)\r\n {\r\n await producer.ProduceEventAsync(confirmEvent);\r\n }\r\n }\r\n}\n"})}),"\n",(0,s.jsx)(r.h2,{id:"subscription-isolation",children:"Subscription Isolation"}),"\n",(0,s.jsx)(r.p,{children:"In a service with mixed internal and external event routing, RCommon allows you to register multiple event handling builders. Each builder maintains its own set of subscribers, so an event published to one transport does not bleed into another."}),"\n",(0,s.jsx)(r.p,{children:"This is useful when:"}),"\n",(0,s.jsxs)(r.ul,{children:["\n",(0,s.jsx)(r.li,{children:"Some events stay within the process (in-memory, for domain event notifications)"}),"\n",(0,s.jsx)(r.li,{children:"Other events cross service boundaries (MassTransit or Wolverine)"}),"\n"]}),"\n",(0,s.jsx)(r.pre,{children:(0,s.jsx)(r.code,{className:"language-csharp",children:'services.AddRCommon()\r\n // In-process events: only handled internally\r\n .WithEventHandling(eventHandling =>\r\n {\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n })\r\n // Cross-service events: routed through the message broker\r\n .WithEventHandling(eventHandling =>\r\n {\r\n eventHandling.UsingRabbitMq((context, cfg) =>\r\n {\r\n cfg.Host("rabbitmq://localhost");\r\n cfg.ConfigureEndpoints(context);\r\n });\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n });\n'})}),"\n",(0,s.jsxs)(r.p,{children:["Events subscribed only in ",(0,s.jsx)(r.code,{children:"InMemoryEventBusBuilder"})," will not be published to the RabbitMQ exchange and vice versa."]}),"\n",(0,s.jsx)(r.h2,{id:"wolverine-alternative",children:"Wolverine Alternative"}),"\n",(0,s.jsx)(r.p,{children:"For services using Wolverine as the message transport, the configuration pattern is identical \u2014 only the builder type changes:"}),"\n",(0,s.jsx)(r.pre,{children:(0,s.jsx)(r.code,{className:"language-csharp",children:'// In Program.cs, before AddRCommon\r\nbuilder.Host.UseWolverine(options =>\r\n{\r\n options.UseRabbitMq(rabbit =>\r\n {\r\n rabbit.HostName = "localhost";\r\n }).AutoProvision();\r\n});\r\n\r\n// Then in services\r\nservices.AddRCommon()\r\n .WithEventHandling(eventHandling =>\r\n {\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n });\n'})}),"\n",(0,s.jsx)(r.h2,{id:"sharing-infrastructure-abstractions-across-services",children:"Sharing Infrastructure Abstractions Across Services"}),"\n",(0,s.jsx)(r.p,{children:"Because RCommon's abstractions are transport-agnostic, a shared application services library can be referenced by multiple services without binding them to a specific message broker. Each service registers its own concrete implementations at startup."}),"\n",(0,s.jsx)(r.pre,{children:(0,s.jsx)(r.code,{className:"language-csharp",children:"// Shared library\r\npublic interface IOrderEventPublisher\r\n{\r\n Task PublishOrderPlacedAsync(OrderPlacedEvent @event);\r\n}\r\n\r\n// Service A: MassTransit implementation\r\npublic class MassTransitOrderEventPublisher : IOrderEventPublisher\r\n{\r\n private readonly IEnumerable _producers;\r\n\r\n public MassTransitOrderEventPublisher(IEnumerable producers)\r\n => _producers = producers;\r\n\r\n public async Task PublishOrderPlacedAsync(OrderPlacedEvent @event)\r\n {\r\n foreach (var producer in _producers)\r\n await producer.ProduceEventAsync(@event);\r\n }\r\n}\r\n\r\n// Service B: Wolverine implementation\r\npublic class WolverineOrderEventPublisher : IOrderEventPublisher\r\n{\r\n private readonly IEnumerable _producers;\r\n\r\n public WolverineOrderEventPublisher(IEnumerable producers)\r\n => _producers = producers;\r\n\r\n public async Task PublishOrderPlacedAsync(OrderPlacedEvent @event)\r\n {\r\n foreach (var producer in _producers)\r\n await producer.ProduceEventAsync(@event);\r\n }\r\n}\n"})}),"\n",(0,s.jsxs)(r.p,{children:["Both implementations are structurally identical. The difference is which ",(0,s.jsx)(r.code,{children:"IEventProducer"})," is injected, determined by the ",(0,s.jsx)(r.code,{children:"WithEventHandling<>"})," call in ",(0,s.jsx)(r.code,{children:"Program.cs"}),"."]}),"\n",(0,s.jsx)(r.h2,{id:"per-service-persistence",children:"Per-Service Persistence"}),"\n",(0,s.jsxs)(r.p,{children:["Each microservice owns its own database and schema. The ",(0,s.jsx)(r.code,{children:"WithPersistence<>"})," builder call and ",(0,s.jsx)(r.code,{children:"DbContext"})," registration are scoped to the service:"]}),"\n",(0,s.jsx)(r.pre,{children:(0,s.jsx)(r.code,{className:"language-csharp",children:'// Order Service\r\nservices.AddRCommon()\r\n .WithPersistence(ef =>\r\n {\r\n ef.AddDbContext("Orders", options =>\r\n {\r\n options.UseSqlServer(config.GetConnectionString("Orders"));\r\n });\r\n ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "Orders");\r\n });\r\n\r\n// Inventory Service\r\nservices.AddRCommon()\r\n .WithPersistence(ef =>\r\n {\r\n ef.AddDbContext("Inventory", options =>\r\n {\r\n options.UseNpgsql(config.GetConnectionString("Inventory"));\r\n });\r\n ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "Inventory");\r\n });\n'})}),"\n",(0,s.jsxs)(r.p,{children:["The Order Service uses SQL Server; the Inventory Service uses PostgreSQL. Both use the same ",(0,s.jsx)(r.code,{children:"IGraphRepository"})," interface in their handlers."]}),"\n",(0,s.jsx)(r.h2,{id:"health-considerations",children:"Health Considerations"}),"\n",(0,s.jsxs)(r.ul,{children:["\n",(0,s.jsx)(r.li,{children:"Register transport-specific health checks alongside your normal ASP.NET health checks"}),"\n",(0,s.jsxs)(r.li,{children:["Use ",(0,s.jsx)(r.code,{children:"WithUnitOfWork"})," in services that need transactional consistency around persistence and event publishing"]}),"\n",(0,s.jsx)(r.li,{children:"Consider idempotency in handlers \u2014 MassTransit and Wolverine may redeliver messages on failure"}),"\n"]}),"\n",(0,s.jsx)(r.h2,{id:"api-reference",children:"API Reference"}),"\n",(0,s.jsxs)(r.table,{children:[(0,s.jsx)(r.thead,{children:(0,s.jsxs)(r.tr,{children:[(0,s.jsx)(r.th,{children:"Type"}),(0,s.jsx)(r.th,{children:"Package"}),(0,s.jsx)(r.th,{children:"Purpose"})]})}),(0,s.jsxs)(r.tbody,{children:[(0,s.jsxs)(r.tr,{children:[(0,s.jsx)(r.td,{children:(0,s.jsx)(r.code,{children:"ISyncEvent"})}),(0,s.jsx)(r.td,{children:(0,s.jsx)(r.code,{children:"RCommon.Models"})}),(0,s.jsx)(r.td,{children:"Base interface for all events"})]}),(0,s.jsxs)(r.tr,{children:[(0,s.jsx)(r.td,{children:(0,s.jsx)(r.code,{children:"IEventProducer"})}),(0,s.jsx)(r.td,{children:(0,s.jsx)(r.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(r.td,{children:"Produces events to a transport"})]}),(0,s.jsxs)(r.tr,{children:[(0,s.jsx)(r.td,{children:(0,s.jsx)(r.code,{children:"ISubscriber"})}),(0,s.jsx)(r.td,{children:(0,s.jsx)(r.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(r.td,{children:"Handles events from a transport"})]}),(0,s.jsxs)(r.tr,{children:[(0,s.jsx)(r.td,{children:(0,s.jsx)(r.code,{children:"MassTransitEventHandlingBuilder"})}),(0,s.jsx)(r.td,{children:(0,s.jsx)(r.code,{children:"RCommon.MassTransit"})}),(0,s.jsx)(r.td,{children:"Configures MassTransit as the event transport"})]}),(0,s.jsxs)(r.tr,{children:[(0,s.jsx)(r.td,{children:(0,s.jsx)(r.code,{children:"WolverineEventHandlingBuilder"})}),(0,s.jsx)(r.td,{children:(0,s.jsx)(r.code,{children:"RCommon.Wolverine"})}),(0,s.jsx)(r.td,{children:"Configures Wolverine as the event transport"})]}),(0,s.jsxs)(r.tr,{children:[(0,s.jsx)(r.td,{children:(0,s.jsx)(r.code,{children:"InMemoryEventBusBuilder"})}),(0,s.jsx)(r.td,{children:(0,s.jsx)(r.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(r.td,{children:"In-process event bus (no external broker)"})]}),(0,s.jsxs)(r.tr,{children:[(0,s.jsx)(r.td,{children:(0,s.jsx)(r.code,{children:"PublishWithMassTransitEventProducer"})}),(0,s.jsx)(r.td,{children:(0,s.jsx)(r.code,{children:"RCommon.MassTransit"})}),(0,s.jsx)(r.td,{children:"MassTransit-backed event producer"})]}),(0,s.jsxs)(r.tr,{children:[(0,s.jsx)(r.td,{children:(0,s.jsx)(r.code,{children:"PublishWithWolverineEventProducer"})}),(0,s.jsx)(r.td,{children:(0,s.jsx)(r.code,{children:"RCommon.Wolverine"})}),(0,s.jsx)(r.td,{children:"Wolverine-backed event producer"})]})]})]})]})}function u(e={}){const{wrapper:r}={...(0,t.R)(),...e.components};return r?(0,s.jsx)(r,{...e,children:(0,s.jsx)(l,{...e})}):l(e)}},64996(e,r,n){n.d(r,{A:()=>o});n(30758);var s=n(13526);const t="tabItem_jA3u";var i=n(86070);function o({children:e,hidden:r,className:n}){return(0,i.jsx)("div",{role:"tabpanel",className:(0,s.A)(t,n),hidden:r,children:e})}},85363(e,r,n){n.d(r,{A:()=>P});var s=n(30758),t=n(13526),i=n(32416),o=n(25557),a=n(85924),c=n(64493),d=n(10274),l=n(44118);function u(e){return s.Children.toArray(e).filter(e=>"\n"!==e).map(e=>{if(!e||(0,s.isValidElement)(e)&&function(e){const{props:r}=e;return!!r&&"object"==typeof r&&"value"in r}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)})?.filter(Boolean)??[]}function h(e){const{values:r,children:n}=e;return(0,s.useMemo)(()=>{const e=r??function(e){return u(e).map(({props:{value:e,label:r,attributes:n,default:s}})=>({value:e,label:r,attributes:n,default:s}))}(n);return function(e){const r=(0,d.X)(e,(e,r)=>e.value===r.value);if(r.length>0)throw new Error(`Docusaurus error: Duplicate values "${r.map(e=>e.value).join(", ")}" found in . Every value needs to be unique.`)}(e),e},[r,n])}function v({value:e,tabValues:r}){return r.some(r=>r.value===e)}function p({queryString:e=!1,groupId:r}){const n=(0,o.W6)(),t=function({queryString:e=!1,groupId:r}){if("string"==typeof e)return e;if(!1===e)return null;if(!0===e&&!r)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return r??null}({queryString:e,groupId:r});return[(0,c.aZ)(t),(0,s.useCallback)(e=>{if(!t)return;const r=new URLSearchParams(n.location.search);r.set(t,e),n.replace({...n.location,search:r.toString()})},[t,n])]}function m(e){const{defaultValue:r,queryString:n=!1,groupId:t}=e,i=h(e),[o,c]=(0,s.useState)(()=>function({defaultValue:e,tabValues:r}){if(0===r.length)throw new Error("Docusaurus error: the component requires at least one children component");if(e){if(!v({value:e,tabValues:r}))throw new Error(`Docusaurus error: The has a defaultValue "${e}" but none of its children has the corresponding value. Available values are: ${r.map(e=>e.value).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return e}const n=r.find(e=>e.default)??r[0];if(!n)throw new Error("Unexpected error: 0 tabValues");return n.value}({defaultValue:r,tabValues:i})),[d,u]=p({queryString:n,groupId:t}),[m,b]=function({groupId:e}){const r=function(e){return e?`docusaurus.tab.${e}`:null}(e),[n,t]=(0,l.Dv)(r);return[n,(0,s.useCallback)(e=>{r&&t.set(e)},[r,t])]}({groupId:t}),g=(()=>{const e=d??m;return v({value:e,tabValues:i})?e:null})();(0,a.A)(()=>{g&&c(g)},[g]);return{selectedValue:o,selectValue:(0,s.useCallback)(e=>{if(!v({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);c(e),u(e),b(e)},[u,b,i]),tabValues:i}}var b=n(81264);const g="tabList_TfjZ",x="tabItem_vVs9";var f=n(86070);function j({className:e,block:r,selectedValue:n,selectValue:s,tabValues:o}){const a=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.a_)(),d=e=>{const r=e.currentTarget,t=a.indexOf(r),i=o[t].value;i!==n&&(c(r),s(i))},l=e=>{let r=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const n=a.indexOf(e.currentTarget)+1;r=a[n]??a[0];break}case"ArrowLeft":{const n=a.indexOf(e.currentTarget)-1;r=a[n]??a[a.length-1];break}}r?.focus()};return(0,f.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,t.A)("tabs",{"tabs--block":r},e),children:o.map(({value:e,label:r,attributes:s})=>(0,f.jsx)("li",{role:"tab",tabIndex:n===e?0:-1,"aria-selected":n===e,ref:e=>a.push(e),onKeyDown:l,onClick:d,...s,className:(0,t.A)("tabs__item",x,s?.className,{"tabs__item--active":n===e}),children:r??e},e))})}function y({lazy:e,children:r,selectedValue:n}){const t=(Array.isArray(r)?r:[r]).filter(Boolean);if(e){const e=t.find(e=>e.props.value===n);return e?(0,s.cloneElement)(e,{className:"margin-top--md"}):null}return(0,f.jsx)("div",{className:"margin-top--md",children:t.map((e,r)=>(0,s.cloneElement)(e,{key:r,hidden:e.props.value!==n}))})}function E(e){const r=m(e);return(0,f.jsxs)("div",{className:(0,t.A)("tabs-container",g),children:[(0,f.jsx)(j,{...r,...e}),(0,f.jsx)(y,{...r,...e})]})}function P(e){const r=(0,b.A)();return(0,f.jsx)(E,{...e,children:u(e.children)},String(r))}},81753(e,r,n){n.d(r,{R:()=>o,x:()=>a});var s=n(30758);const t={},i=s.createContext(t);function o(e){const r=s.useContext(i);return s.useMemo(function(){return"function"==typeof e?e(r):{...r,...e}},[r,e])}function a(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),s.createElement(i.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/462845a6.7c1ea3fd.js b/website/build/assets/js/462845a6.7c1ea3fd.js new file mode 100644 index 00000000..f4240380 --- /dev/null +++ b/website/build/assets/js/462845a6.7c1ea3fd.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[5999],{42722(e,n,r){r.r(n),r.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>u,frontMatter:()=>i,metadata:()=>a,toc:()=>d});var s=r(86070),t=r(81753);r(85363),r(64996);const i={title:"Microservices",sidebar_position:2,description:"Build .NET microservices with RCommon using MassTransit or Wolverine for messaging, per-service persistence, and transport-agnostic IEventProducer abstractions."},o="Microservices with RCommon",a={id:"architecture-guides/microservices",title:"Microservices",description:"Build .NET microservices with RCommon using MassTransit or Wolverine for messaging, per-service persistence, and transport-agnostic IEventProducer abstractions.",source:"@site/docs/architecture-guides/microservices.mdx",sourceDirName:"architecture-guides",slug:"/architecture-guides/microservices",permalink:"/docs/next/architecture-guides/microservices",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/architecture-guides/microservices.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Microservices",sidebar_position:2,description:"Build .NET microservices with RCommon using MassTransit or Wolverine for messaging, per-service persistence, and transport-agnostic IEventProducer abstractions."},sidebar:"docsSidebar",previous:{title:"Clean Architecture",permalink:"/docs/next/architecture-guides/clean-architecture"},next:{title:"Event-Driven Architecture",permalink:"/docs/next/architecture-guides/event-driven"}},c={},d=[{value:"When to Use This Approach",id:"when-to-use-this-approach",level:2},{value:"Core Patterns for Microservices",id:"core-patterns-for-microservices",level:2},{value:"Setting Up a Service with Messaging",id:"setting-up-a-service-with-messaging",level:2},{value:"Defining Events as Shared Contracts",id:"defining-events-as-shared-contracts",level:2},{value:"Publishing Events",id:"publishing-events",level:2},{value:"Subscribing to Events",id:"subscribing-to-events",level:2},{value:"Subscription Isolation",id:"subscription-isolation",level:2},{value:"Wolverine Alternative",id:"wolverine-alternative",level:2},{value:"Sharing Infrastructure Abstractions Across Services",id:"sharing-infrastructure-abstractions-across-services",level:2},{value:"Per-Service Persistence",id:"per-service-persistence",level:2},{value:"Health Considerations",id:"health-considerations",level:2},{value:"API Reference",id:"api-reference",level:2}];function l(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"microservices-with-rcommon",children:"Microservices with RCommon"}),"\n",(0,s.jsx)(n.p,{children:"Microservices decompose a system into independently deployable services that communicate over a network. RCommon provides the abstractions and integrations that make this communication consistent, testable, and swappable across a distributed system."}),"\n",(0,s.jsx)(n.h2,{id:"when-to-use-this-approach",children:"When to Use This Approach"}),"\n",(0,s.jsx)(n.p,{children:"Consider a microservices architecture with RCommon when:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Different parts of your domain have different scaling requirements"}),"\n",(0,s.jsx)(n.li,{children:"Separate teams own separate services and need clear contracts between them"}),"\n",(0,s.jsx)(n.li,{children:"You need to evolve service internals without redeploying the entire system"}),"\n",(0,s.jsx)(n.li,{children:"Integration points require durable, reliable messaging (not just HTTP calls)"}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"core-patterns-for-microservices",children:"Core Patterns for Microservices"}),"\n",(0,s.jsx)(n.p,{children:"RCommon supports three primary patterns that are essential in microservice environments:"}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Messaging"})," \u2014 Publish events that other services subscribe to via MassTransit or Wolverine"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Shared abstractions"})," \u2014 ",(0,s.jsx)(n.code,{children:"IGraphRepository"}),", ",(0,s.jsx)(n.code,{children:"IMediatorService"}),", and ",(0,s.jsx)(n.code,{children:"IEventProducer"})," are the same across every service; only the registered implementation changes"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Subscription isolation"})," \u2014 Each service subscribes to exactly the events it needs, routing them to the correct transport"]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"setting-up-a-service-with-messaging",children:"Setting Up a Service with Messaging"}),"\n",(0,s.jsxs)(n.p,{children:["Each microservice calls ",(0,s.jsx)(n.code,{children:"AddRCommon()"})," independently. Inside a single service, feature-folder modules can each call ",(0,s.jsx)(n.code,{children:"AddRCommon().With...(...)"})," again \u2014 the bootstrapper caches the builder and merges registrations across calls. This lets a service's internal modules wire their own subscribers, validators, and DbContexts without funneling everything through one static initializer. See ",(0,s.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the contract that makes this safe."]}),"\n",(0,s.jsx)(n.p,{children:"The following shows how a service is configured to publish and receive messages via MassTransit:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'// Program.cs \u2014 Order Service\nservices.AddRCommon()\n .WithEventHandling(eventHandling =>\n {\n // Configure the transport (RabbitMQ, Azure Service Bus, etc.)\n eventHandling.UsingRabbitMq((context, cfg) =>\n {\n cfg.Host("rabbitmq://localhost");\n cfg.ConfigureEndpoints(context);\n });\n\n // This service produces order events\n eventHandling.AddProducer();\n\n // This service consumes inventory events\n eventHandling.AddSubscriber();\n });\n'})}),"\n",(0,s.jsx)(n.p,{children:"On the inventory service side:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'// Program.cs \u2014 Inventory Service\nservices.AddRCommon()\n .WithEventHandling(eventHandling =>\n {\n eventHandling.UsingRabbitMq((context, cfg) =>\n {\n cfg.Host("rabbitmq://localhost");\n cfg.ConfigureEndpoints(context);\n });\n\n eventHandling.AddProducer();\n\n // This service handles order placed events\n eventHandling.AddSubscriber();\n });\n'})}),"\n",(0,s.jsx)(n.h2,{id:"defining-events-as-shared-contracts",children:"Defining Events as Shared Contracts"}),"\n",(0,s.jsx)(n.p,{children:"Events that cross service boundaries should be defined in a shared contracts library. Both services reference this library, never each other:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"// SharedContracts/OrderPlacedEvent.cs\nusing RCommon.Models.Events;\n\npublic class OrderPlacedEvent : ISyncEvent\n{\n public OrderPlacedEvent() { }\n\n public OrderPlacedEvent(Guid orderId, string customerId, decimal total)\n {\n OrderId = orderId;\n CustomerId = customerId;\n Total = total;\n }\n\n public Guid OrderId { get; }\n public string CustomerId { get; }\n public decimal Total { get; }\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"ISyncEvent"})," is RCommon's base marker interface. The transport-specific serialization and routing is handled by the infrastructure layer \u2014 the event itself stays clean."]}),"\n",(0,s.jsx)(n.h2,{id:"publishing-events",children:"Publishing Events"}),"\n",(0,s.jsxs)(n.p,{children:["Services publish events through ",(0,s.jsx)(n.code,{children:"IEventProducer"}),". The call site does not know which transport is registered:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public class PlaceOrderCommandHandler\n : IAppRequestHandler\n{\n private readonly IGraphRepository _orderRepository;\n private readonly IEnumerable _eventProducers;\n\n public PlaceOrderCommandHandler(\n IGraphRepository orderRepository,\n IEnumerable eventProducers)\n {\n _orderRepository = orderRepository;\n _eventProducers = eventProducers;\n }\n\n public async Task HandleAsync(\n PlaceOrderCommand request,\n CancellationToken cancellationToken)\n {\n var order = new Order(request.CustomerId, request.Items);\n await _orderRepository.AddAsync(order);\n\n var @event = new OrderPlacedEvent(order.Id, order.CustomerId, order.Total);\n\n foreach (var producer in _eventProducers)\n {\n await producer.ProduceEventAsync(@event);\n }\n\n return new BaseCommandResponse { Success = true, Id = order.Id };\n }\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"subscribing-to-events",children:"Subscribing to Events"}),"\n",(0,s.jsxs)(n.p,{children:["Event handlers implement ",(0,s.jsx)(n.code,{children:"ISubscriber"}),". The handler contains only business logic \u2014 no transport concerns:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"// Inventory Service\npublic class ReserveInventoryHandler : ISubscriber\n{\n private readonly IGraphRepository _inventoryRepository;\n private readonly IEnumerable _eventProducers;\n\n public ReserveInventoryHandler(\n IGraphRepository inventoryRepository,\n IEnumerable eventProducers)\n {\n _inventoryRepository = inventoryRepository;\n _eventProducers = eventProducers;\n }\n\n public async Task HandleAsync(\n OrderPlacedEvent notification,\n CancellationToken cancellationToken = default)\n {\n // Reserve inventory items\n var reserved = await ReserveItems(notification.OrderId);\n\n // Emit a confirmation event back\n var confirmEvent = new InventoryReservedEvent(notification.OrderId, reserved);\n foreach (var producer in _eventProducers)\n {\n await producer.ProduceEventAsync(confirmEvent);\n }\n }\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"subscription-isolation",children:"Subscription Isolation"}),"\n",(0,s.jsx)(n.p,{children:"In a service with mixed internal and external event routing, RCommon allows you to register multiple event handling builders. Each builder maintains its own set of subscribers, so an event published to one transport does not bleed into another."}),"\n",(0,s.jsx)(n.p,{children:"This is useful when:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Some events stay within the process (in-memory, for domain event notifications)"}),"\n",(0,s.jsx)(n.li,{children:"Other events cross service boundaries (MassTransit or Wolverine)"}),"\n"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'services.AddRCommon()\n // In-process events: only handled internally\n .WithEventHandling(eventHandling =>\n {\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n })\n // Cross-service events: routed through the message broker\n .WithEventHandling(eventHandling =>\n {\n eventHandling.UsingRabbitMq((context, cfg) =>\n {\n cfg.Host("rabbitmq://localhost");\n cfg.ConfigureEndpoints(context);\n });\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n });\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Events subscribed only in ",(0,s.jsx)(n.code,{children:"InMemoryEventBusBuilder"})," will not be published to the RabbitMQ exchange and vice versa."]}),"\n",(0,s.jsx)(n.h2,{id:"wolverine-alternative",children:"Wolverine Alternative"}),"\n",(0,s.jsx)(n.p,{children:"For services using Wolverine as the message transport, the configuration pattern is identical \u2014 only the builder type changes:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'// In Program.cs, before AddRCommon\nbuilder.Host.UseWolverine(options =>\n{\n options.UseRabbitMq(rabbit =>\n {\n rabbit.HostName = "localhost";\n }).AutoProvision();\n});\n\n// Then in services\nservices.AddRCommon()\n .WithEventHandling(eventHandling =>\n {\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n });\n'})}),"\n",(0,s.jsx)(n.h2,{id:"sharing-infrastructure-abstractions-across-services",children:"Sharing Infrastructure Abstractions Across Services"}),"\n",(0,s.jsx)(n.p,{children:"Because RCommon's abstractions are transport-agnostic, a shared application services library can be referenced by multiple services without binding them to a specific message broker. Each service registers its own concrete implementations at startup."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"// Shared library\npublic interface IOrderEventPublisher\n{\n Task PublishOrderPlacedAsync(OrderPlacedEvent @event);\n}\n\n// Service A: MassTransit implementation\npublic class MassTransitOrderEventPublisher : IOrderEventPublisher\n{\n private readonly IEnumerable _producers;\n\n public MassTransitOrderEventPublisher(IEnumerable producers)\n => _producers = producers;\n\n public async Task PublishOrderPlacedAsync(OrderPlacedEvent @event)\n {\n foreach (var producer in _producers)\n await producer.ProduceEventAsync(@event);\n }\n}\n\n// Service B: Wolverine implementation\npublic class WolverineOrderEventPublisher : IOrderEventPublisher\n{\n private readonly IEnumerable _producers;\n\n public WolverineOrderEventPublisher(IEnumerable producers)\n => _producers = producers;\n\n public async Task PublishOrderPlacedAsync(OrderPlacedEvent @event)\n {\n foreach (var producer in _producers)\n await producer.ProduceEventAsync(@event);\n }\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Both implementations are structurally identical. The difference is which ",(0,s.jsx)(n.code,{children:"IEventProducer"})," is injected, determined by the ",(0,s.jsx)(n.code,{children:"WithEventHandling<>"})," call in ",(0,s.jsx)(n.code,{children:"Program.cs"}),"."]}),"\n",(0,s.jsx)(n.h2,{id:"per-service-persistence",children:"Per-Service Persistence"}),"\n",(0,s.jsxs)(n.p,{children:["Each microservice owns its own database and schema. The ",(0,s.jsx)(n.code,{children:"WithPersistence<>"})," builder call and ",(0,s.jsx)(n.code,{children:"DbContext"})," registration are scoped to the service:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'// Order Service\nservices.AddRCommon()\n .WithPersistence(ef =>\n {\n ef.AddDbContext("Orders", options =>\n {\n options.UseSqlServer(config.GetConnectionString("Orders"));\n });\n ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "Orders");\n });\n\n// Inventory Service\nservices.AddRCommon()\n .WithPersistence(ef =>\n {\n ef.AddDbContext("Inventory", options =>\n {\n options.UseNpgsql(config.GetConnectionString("Inventory"));\n });\n ef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "Inventory");\n });\n'})}),"\n",(0,s.jsxs)(n.p,{children:["The Order Service uses SQL Server; the Inventory Service uses PostgreSQL. Both use the same ",(0,s.jsx)(n.code,{children:"IGraphRepository"})," interface in their handlers."]}),"\n",(0,s.jsx)(n.h2,{id:"health-considerations",children:"Health Considerations"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Register transport-specific health checks alongside your normal ASP.NET health checks"}),"\n",(0,s.jsxs)(n.li,{children:["Use ",(0,s.jsx)(n.code,{children:"WithUnitOfWork"})," in services that need transactional consistency around persistence and event publishing"]}),"\n",(0,s.jsx)(n.li,{children:"Consider idempotency in handlers \u2014 MassTransit and Wolverine may redeliver messages on failure"}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"api-reference",children:"API Reference"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Package"}),(0,s.jsx)(n.th,{children:"Purpose"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"ISyncEvent"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.Models"})}),(0,s.jsx)(n.td,{children:"Base interface for all events"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"IEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(n.td,{children:"Produces events to a transport"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"ISubscriber"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(n.td,{children:"Handles events from a transport"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,s.jsx)(n.td,{children:"Configures MassTransit as the event transport"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.Wolverine"})}),(0,s.jsx)(n.td,{children:"Configures Wolverine as the event transport"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"InMemoryEventBusBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(n.td,{children:"In-process event bus (no external broker)"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,s.jsx)(n.td,{children:"MassTransit-backed event producer"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.Wolverine"})}),(0,s.jsx)(n.td,{children:"Wolverine-backed event producer"})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(l,{...e})}):l(e)}},64996(e,n,r){r.d(n,{A:()=>o});r(30758);var s=r(13526);const t="tabItem_jA3u";var i=r(86070);function o({children:e,hidden:n,className:r}){return(0,i.jsx)("div",{role:"tabpanel",className:(0,s.A)(t,r),hidden:n,children:e})}},85363(e,n,r){r.d(n,{A:()=>P});var s=r(30758),t=r(13526),i=r(32416),o=r(25557),a=r(85924),c=r(64493),d=r(10274),l=r(44118);function u(e){return s.Children.toArray(e).filter(e=>"\n"!==e).map(e=>{if(!e||(0,s.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)})?.filter(Boolean)??[]}function h(e){const{values:n,children:r}=e;return(0,s.useMemo)(()=>{const e=n??function(e){return u(e).map(({props:{value:e,label:n,attributes:r,default:s}})=>({value:e,label:n,attributes:r,default:s}))}(r);return function(e){const n=(0,d.X)(e,(e,n)=>e.value===n.value);if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map(e=>e.value).join(", ")}" found in . Every value needs to be unique.`)}(e),e},[n,r])}function v({value:e,tabValues:n}){return n.some(n=>n.value===e)}function p({queryString:e=!1,groupId:n}){const r=(0,o.W6)(),t=function({queryString:e=!1,groupId:n}){if("string"==typeof e)return e;if(!1===e)return null;if(!0===e&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:e,groupId:n});return[(0,c.aZ)(t),(0,s.useCallback)(e=>{if(!t)return;const n=new URLSearchParams(r.location.search);n.set(t,e),r.replace({...r.location,search:n.toString()})},[t,r])]}function m(e){const{defaultValue:n,queryString:r=!1,groupId:t}=e,i=h(e),[o,c]=(0,s.useState)(()=>function({defaultValue:e,tabValues:n}){if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(e){if(!v({value:e,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${e}" but none of its children has the corresponding value. Available values are: ${n.map(e=>e.value).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return e}const r=n.find(e=>e.default)??n[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:n,tabValues:i})),[d,u]=p({queryString:r,groupId:t}),[m,b]=function({groupId:e}){const n=function(e){return e?`docusaurus.tab.${e}`:null}(e),[r,t]=(0,l.Dv)(n);return[r,(0,s.useCallback)(e=>{n&&t.set(e)},[n,t])]}({groupId:t}),g=(()=>{const e=d??m;return v({value:e,tabValues:i})?e:null})();(0,a.A)(()=>{g&&c(g)},[g]);return{selectedValue:o,selectValue:(0,s.useCallback)(e=>{if(!v({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);c(e),u(e),b(e)},[u,b,i]),tabValues:i}}var b=r(81264);const g="tabList_TfjZ",x="tabItem_vVs9";var f=r(86070);function j({className:e,block:n,selectedValue:r,selectValue:s,tabValues:o}){const a=[],{blockElementScrollPositionUntilNextRender:c}=(0,i.a_)(),d=e=>{const n=e.currentTarget,t=a.indexOf(n),i=o[t].value;i!==r&&(c(n),s(i))},l=e=>{let n=null;switch(e.key){case"Enter":d(e);break;case"ArrowRight":{const r=a.indexOf(e.currentTarget)+1;n=a[r]??a[0];break}case"ArrowLeft":{const r=a.indexOf(e.currentTarget)-1;n=a[r]??a[a.length-1];break}}n?.focus()};return(0,f.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,t.A)("tabs",{"tabs--block":n},e),children:o.map(({value:e,label:n,attributes:s})=>(0,f.jsx)("li",{role:"tab",tabIndex:r===e?0:-1,"aria-selected":r===e,ref:e=>a.push(e),onKeyDown:l,onClick:d,...s,className:(0,t.A)("tabs__item",x,s?.className,{"tabs__item--active":r===e}),children:n??e},e))})}function y({lazy:e,children:n,selectedValue:r}){const t=(Array.isArray(n)?n:[n]).filter(Boolean);if(e){const e=t.find(e=>e.props.value===r);return e?(0,s.cloneElement)(e,{className:"margin-top--md"}):null}return(0,f.jsx)("div",{className:"margin-top--md",children:t.map((e,n)=>(0,s.cloneElement)(e,{key:n,hidden:e.props.value!==r}))})}function E(e){const n=m(e);return(0,f.jsxs)("div",{className:(0,t.A)("tabs-container",g),children:[(0,f.jsx)(j,{...n,...e}),(0,f.jsx)(y,{...n,...e})]})}function P(e){const n=(0,b.A)();return(0,f.jsx)(E,{...e,children:u(e.children)},String(n))}},81753(e,n,r){r.d(n,{R:()=>o,x:()=>a});var s=r(30758);const t={},i=s.createContext(t);function o(e){const n=s.useContext(i);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/51ddfa24.0b2fd76c.js b/website/build/assets/js/51ddfa24.0b2fd76c.js deleted file mode 100644 index f0d5a075..00000000 --- a/website/build/assets/js/51ddfa24.0b2fd76c.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[4444],{71505(e,n,r){r.r(n),r.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>u,frontMatter:()=>i,metadata:()=>o,toc:()=>c});var t=r(86070),a=r(81753);r(85363),r(64996);const i={title:"Clean Architecture",sidebar_position:1,description:"Build .NET apps with Clean Architecture using RCommon. Layer domain, application, and infrastructure with testable CQRS handlers and swappable persistence providers."},s="Clean Architecture with RCommon",o={id:"architecture-guides/clean-architecture",title:"Clean Architecture",description:"Build .NET apps with Clean Architecture using RCommon. Layer domain, application, and infrastructure with testable CQRS handlers and swappable persistence providers.",source:"@site/docs/architecture-guides/clean-architecture.mdx",sourceDirName:"architecture-guides",slug:"/architecture-guides/clean-architecture",permalink:"/docs/next/architecture-guides/clean-architecture",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/architecture-guides/clean-architecture.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Clean Architecture",sidebar_position:1,description:"Build .NET apps with Clean Architecture using RCommon. Layer domain, application, and infrastructure with testable CQRS handlers and swappable persistence providers."},sidebar:"docsSidebar",previous:{title:"Architecture Guides",permalink:"/docs/next/category/architecture-guides"},next:{title:"Microservices",permalink:"/docs/next/architecture-guides/microservices"}},l={},c=[{value:"When to Use This Approach",id:"when-to-use-this-approach",level:2},{value:"Layer Overview",id:"layer-overview",level:2},{value:"Project Structure",id:"project-structure",level:2},{value:"The Domain Layer",id:"the-domain-layer",level:2},{value:"Specifications",id:"specifications",level:3},{value:"The Application Layer",id:"the-application-layer",level:2},{value:"Commands",id:"commands",level:3},{value:"Queries",id:"queries",level:3},{value:"Cross-Cutting Concerns in the Application Layer",id:"cross-cutting-concerns-in-the-application-layer",level:3},{value:"The Infrastructure Layer",id:"the-infrastructure-layer",level:2},{value:"The Composition Root (Program.cs)",id:"the-composition-root-programcs",level:2},{value:"The Presentation Layer",id:"the-presentation-layer",level:2},{value:"Testing Application Handlers",id:"testing-application-handlers",level:2},{value:"Key Design Decisions",id:"key-design-decisions",level:2},{value:"API Reference",id:"api-reference",level:2}];function d(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,a.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"clean-architecture-with-rcommon",children:"Clean Architecture with RCommon"}),"\n",(0,t.jsx)(n.p,{children:"Clean Architecture organizes code into concentric layers where the dependency rule states that source code dependencies can only point inward. The domain and application layers have no knowledge of databases, frameworks, or external systems. RCommon supports this structure by providing infrastructure abstractions that your application layer depends on via interfaces, with concrete implementations registered at the composition root."}),"\n",(0,t.jsx)(n.h2,{id:"when-to-use-this-approach",children:"When to Use This Approach"}),"\n",(0,t.jsx)(n.p,{children:"Use Clean Architecture with RCommon when:"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"Your domain logic is complex enough to warrant isolation from infrastructure concerns"}),"\n",(0,t.jsx)(n.li,{children:"You want to swap persistence providers (EF Core to NHibernate, SQL to NoSQL) without rewriting business logic"}),"\n",(0,t.jsx)(n.li,{children:"You need testable handlers that do not depend on a real database or message broker"}),"\n",(0,t.jsx)(n.li,{children:"Multiple teams work on different layers independently"}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"layer-overview",children:"Layer Overview"}),"\n",(0,t.jsx)(n.p,{children:"Clean Architecture splits the codebase into four layers. With RCommon, the boundaries map as follows:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"+-------------------------------+\r\n| Presentation | ASP.NET Core Controllers / MVC\r\n+-------------------------------+\r\n| Application | Command/Query Handlers, DTOs, Validation\r\n+-------------------------------+\r\n| Domain | Entities, Specifications, Domain Events\r\n+-------------------------------+\r\n| Infrastructure | EF Core DbContext, Identity, Email, etc.\r\n+-------------------------------+\n"})}),"\n",(0,t.jsx)(n.p,{children:"Dependency direction: Presentation -> Application -> Domain. Infrastructure implements the interfaces defined in Application."}),"\n",(0,t.jsx)(n.h2,{id:"project-structure",children:"Project Structure"}),"\n",(0,t.jsx)(n.p,{children:"The HR Leave Management sample included with RCommon demonstrates this layout:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{children:"HR.LeaveManagement.Domain/\r\n LeaveType.cs\r\n LeaveRequest.cs\r\n LeaveAllocation.cs\r\n Common/BaseDomainEntity.cs\r\n Specifications/AllocationExistsSpec.cs\r\n\r\nHR.LeaveManagement.Application/\r\n Features/\r\n LeaveTypes/\r\n Requests/Commands/CreateLeaveTypeCommand.cs\r\n Requests/Queries/GetLeaveTypeListRequest.cs\r\n Handlers/Commands/CreateLeaveTypeCommandHandler.cs\r\n Handlers/Queries/GetLeaveTypeListRequestHandler.cs\r\n DTOs/LeaveType/\r\n Contracts/Identity/IUserService.cs\r\n ApplicationServicesRegistration.cs\r\n\r\nHR.LeaveManagement.Persistence/\r\n LeaveManagementDbContext.cs\r\n Configurations/\r\n\r\nHR.LeaveManagement.Identity/\r\n IdentityServicesRegistration.cs\r\n Services/AuthService.cs\r\n\r\nHR.LeaveManagement.API/\r\n Controllers/LeaveTypesController.cs\r\n Program.cs\n"})}),"\n",(0,t.jsx)(n.p,{children:"Each project references only what is allowed by the dependency rule. The Domain project has no external references beyond RCommon.Entities. The Application project references Domain but not Persistence or Identity."}),"\n",(0,t.jsx)(n.h2,{id:"the-domain-layer",children:"The Domain Layer"}),"\n",(0,t.jsxs)(n.p,{children:["Domain entities inherit from RCommon's ",(0,t.jsx)(n.code,{children:"AuditedEntity"})," base class, which provides auditing fields (created by, modified by, timestamps) automatically."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// HR.LeaveManagement.Domain/Common/BaseDomainEntity.cs\r\nusing RCommon.Entities;\r\n\r\nnamespace HR.LeaveManagement.Domain.Common\r\n{\r\n public abstract class BaseDomainEntity : AuditedEntity\r\n {\r\n }\r\n}\r\n\r\n// HR.LeaveManagement.Domain/LeaveType.cs\r\npublic class LeaveType : BaseDomainEntity\r\n{\r\n public string Name { get; set; }\r\n public int DefaultDays { get; set; }\r\n}\r\n\r\n// HR.LeaveManagement.Domain/LeaveRequest.cs\r\npublic class LeaveRequest : BaseDomainEntity\r\n{\r\n public DateTime StartDate { get; set; }\r\n public DateTime EndDate { get; set; }\r\n public LeaveType LeaveType { get; set; }\r\n public int LeaveTypeId { get; set; }\r\n public DateTime DateRequested { get; set; }\r\n public string RequestComments { get; set; }\r\n public bool? Approved { get; set; }\r\n public bool Cancelled { get; set; }\r\n public string RequestingEmployeeId { get; set; }\r\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"specifications",children:"Specifications"}),"\n",(0,t.jsxs)(n.p,{children:["Domain specifications encapsulate business query logic as reusable, composable objects. RCommon's ",(0,t.jsx)(n.code,{children:"Specification"})," provides this:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// HR.LeaveManagement.Domain/Specifications/AllocationExistsSpec.cs\r\nusing RCommon;\r\n\r\npublic class AllocationExistsSpec : Specification\r\n{\r\n public AllocationExistsSpec(string userId, int leaveTypeId, int period)\r\n : base(q => q.EmployeeId == userId\r\n && q.LeaveTypeId == leaveTypeId\r\n && q.Period == period)\r\n {\r\n }\r\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"The specification is used in the Application layer without any knowledge of EF Core or SQL:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"var allocationCount = await _leaveAllocationRepository\r\n .GetCountAsync(new AllocationExistsSpec(emp.Id, leaveType.Id, period));\n"})}),"\n",(0,t.jsx)(n.h2,{id:"the-application-layer",children:"The Application Layer"}),"\n",(0,t.jsxs)(n.p,{children:["The Application layer contains commands, queries, and their handlers. Handlers implement RCommon's ",(0,t.jsx)(n.code,{children:"IAppRequestHandler"})," interface."]}),"\n",(0,t.jsx)(n.h3,{id:"commands",children:"Commands"}),"\n",(0,t.jsx)(n.p,{children:"A command encapsulates the intent to change state:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// CreateLeaveTypeCommand.cs\r\nusing RCommon.Mediator.Subscribers;\r\n\r\npublic class CreateLeaveTypeCommand : IAppRequest\r\n{\r\n public CreateLeaveTypeDto LeaveTypeDto { get; set; }\r\n}\n"})}),"\n",(0,t.jsxs)(n.p,{children:["The handler receives the command, runs validation through ",(0,t.jsx)(n.code,{children:"IValidationService"}),", and persists via the repository interface:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'// CreateLeaveTypeCommandHandler.cs\r\npublic class CreateLeaveTypeCommandHandler\r\n : IAppRequestHandler\r\n{\r\n private readonly IGraphRepository _leaveTypeRepository;\r\n private readonly IValidationService _validationService;\r\n\r\n public CreateLeaveTypeCommandHandler(\r\n IGraphRepository leaveTypeRepository,\r\n IValidationService validationService)\r\n {\r\n _leaveTypeRepository = leaveTypeRepository;\r\n _validationService = validationService;\r\n _leaveTypeRepository.DataStoreName = DataStoreNamesConst.LeaveManagement;\r\n }\r\n\r\n public async Task HandleAsync(\r\n CreateLeaveTypeCommand request,\r\n CancellationToken cancellationToken)\r\n {\r\n var response = new BaseCommandResponse();\r\n var validationResult = await _validationService.ValidateAsync(request.LeaveTypeDto);\r\n\r\n if (!validationResult.IsValid)\r\n {\r\n response.Success = false;\r\n response.Message = "Creation Failed";\r\n response.Errors = validationResult.Errors\r\n .Select(q => q.ErrorMessage).ToList();\r\n }\r\n else\r\n {\r\n var leaveType = request.LeaveTypeDto.ToLeaveType();\r\n await _leaveTypeRepository.AddAsync(leaveType);\r\n response.Success = true;\r\n response.Message = "Creation Successful";\r\n response.Id = leaveType.Id;\r\n }\r\n\r\n return response;\r\n }\r\n}\n'})}),"\n",(0,t.jsx)(n.h3,{id:"queries",children:"Queries"}),"\n",(0,t.jsx)(n.p,{children:"Queries return data without modifying state:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// GetLeaveTypeListRequestHandler.cs\r\npublic class GetLeaveTypeListRequestHandler\r\n : IAppRequestHandler>\r\n{\r\n private readonly IGraphRepository _leaveTypeRepository;\r\n\r\n public GetLeaveTypeListRequestHandler(IGraphRepository leaveTypeRepository)\r\n {\r\n _leaveTypeRepository = leaveTypeRepository;\r\n _leaveTypeRepository.DataStoreName = DataStoreNamesConst.LeaveManagement;\r\n }\r\n\r\n public async Task> HandleAsync(\r\n GetLeaveTypeListRequest request,\r\n CancellationToken cancellationToken)\r\n {\r\n var leaveTypes = await _leaveTypeRepository.FindAsync(x => true);\r\n return leaveTypes.Select(x => x.ToLeaveTypeDto()).ToList();\r\n }\r\n}\n"})}),"\n",(0,t.jsx)(n.h3,{id:"cross-cutting-concerns-in-the-application-layer",children:"Cross-Cutting Concerns in the Application Layer"}),"\n",(0,t.jsx)(n.p,{children:"The Application layer defines contracts for cross-cutting concerns as interfaces. Infrastructure implements them:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// Contracts/Identity/IUserService.cs\r\npublic interface IUserService\r\n{\r\n Task> GetEmployees();\r\n}\r\n\r\n// Contracts/Identity/IAuthService.cs\r\npublic interface IAuthService\r\n{\r\n Task Login(AuthRequest request);\r\n Task Register(RegistrationRequest request);\r\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"the-infrastructure-layer",children:"The Infrastructure Layer"}),"\n",(0,t.jsxs)(n.p,{children:["The ",(0,t.jsx)(n.code,{children:"LeaveManagementDbContext"})," inherits from RCommon's ",(0,t.jsx)(n.code,{children:"AuditableDbContext"}),", which automatically stamps the ",(0,t.jsx)(n.code,{children:"CreatedBy"}),", ",(0,t.jsx)(n.code,{children:"ModifiedBy"}),", ",(0,t.jsx)(n.code,{children:"CreateDate"}),", and ",(0,t.jsx)(n.code,{children:"ModifyDate"})," fields on every save operation:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"// HR.LeaveManagement.Persistence/LeaveManagementDbContext.cs\r\npublic class LeaveManagementDbContext : AuditableDbContext\r\n{\r\n public LeaveManagementDbContext(\r\n DbContextOptions options,\r\n ICurrentUser currentUser,\r\n ISystemTime systemTime)\r\n : base(options, currentUser, systemTime)\r\n {\r\n }\r\n\r\n protected override void OnModelCreating(ModelBuilder modelBuilder)\r\n {\r\n modelBuilder.ApplyConfigurationsFromAssembly(\r\n typeof(LeaveManagementDbContext).Assembly);\r\n }\r\n\r\n public DbSet LeaveRequests { get; set; }\r\n public DbSet LeaveTypes { get; set; }\r\n public DbSet LeaveAllocations { get; set; }\r\n}\n"})}),"\n",(0,t.jsx)(n.h2,{id:"the-composition-root-programcs",children:"The Composition Root (Program.cs)"}),"\n",(0,t.jsxs)(n.p,{children:["All layers are wired together in ",(0,t.jsx)(n.code,{children:"Program.cs"})," using RCommon's fluent builder. This is the only place where concrete implementations are named:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithClaimsAndPrincipalAccessor()\r\n .WithSendGridEmailServices(x =>\r\n {\r\n var sendGridSettings = builder.Configuration.Get();\r\n x.SendGridApiKey = sendGridSettings.SendGridApiKey;\r\n x.FromNameDefault = sendGridSettings.FromNameDefault;\r\n x.FromEmailDefault = sendGridSettings.FromEmailDefault;\r\n })\r\n .WithDateTimeSystem(dateTime => dateTime.Kind = DateTimeKind.Utc)\r\n .WithSequentialGuidGenerator(guid =>\r\n guid.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString)\r\n .WithMediator(mediator =>\r\n {\r\n mediator.AddRequest();\r\n mediator.AddRequest,\r\n GetLeaveTypeListRequestHandler>();\r\n // ... register all handlers\r\n\r\n mediator.Configure(config =>\r\n {\r\n config.RegisterServicesFromAssemblies(\r\n typeof(ApplicationServicesRegistration).GetTypeInfo().Assembly);\r\n });\r\n mediator.AddLoggingToRequestPipeline();\r\n mediator.AddUnitOfWorkToRequestPipeline();\r\n })\r\n .WithPersistence(ef =>\r\n {\r\n ef.AddDbContext(\r\n DataStoreNamesConst.LeaveManagement,\r\n options =>\r\n {\r\n options.UseSqlServer(\r\n builder.Configuration.GetConnectionString(\r\n DataStoreNamesConst.LeaveManagement));\r\n });\r\n ef.SetDefaultDataStore(dataStore =>\r\n {\r\n dataStore.DefaultDataStoreName = DataStoreNamesConst.LeaveManagement;\r\n });\r\n })\r\n .WithUnitOfWork(unitOfWork =>\r\n {\r\n unitOfWork.SetOptions(options =>\r\n {\r\n options.AutoCompleteScope = true;\r\n options.DefaultIsolation = IsolationLevel.ReadCommitted;\r\n });\r\n })\r\n .WithValidation(validation =>\r\n {\r\n validation.AddValidatorsFromAssemblyContaining(\r\n typeof(ApplicationServicesRegistration));\r\n });\n"})}),"\n",(0,t.jsx)(n.h2,{id:"the-presentation-layer",children:"The Presentation Layer"}),"\n",(0,t.jsxs)(n.p,{children:["Controllers depend only on ",(0,t.jsx)(n.code,{children:"IMediatorService"}),". They dispatch commands and queries without knowing which handler handles them:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'[Route("api/[controller]")]\r\n[ApiController]\r\n[Authorize]\r\npublic class LeaveTypesController : ControllerBase\r\n{\r\n private readonly IMediatorService _mediator;\r\n\r\n public LeaveTypesController(IMediatorService mediator)\r\n {\r\n _mediator = mediator;\r\n }\r\n\r\n [HttpGet]\r\n public async Task>> Get()\r\n {\r\n var leaveTypes = await _mediator.Send>(\r\n new GetLeaveTypeListRequest());\r\n return Ok(leaveTypes);\r\n }\r\n\r\n [HttpPost]\r\n [Authorize(Roles = "Administrator")]\r\n public async Task> Post(\r\n [FromBody] CreateLeaveTypeDto leaveType)\r\n {\r\n var command = new CreateLeaveTypeCommand { LeaveTypeDto = leaveType };\r\n var response = await _mediator.Send(command);\r\n return Ok(response);\r\n }\r\n}\n'})}),"\n",(0,t.jsx)(n.h2,{id:"testing-application-handlers",children:"Testing Application Handlers"}),"\n",(0,t.jsx)(n.p,{children:"Because handlers depend only on interfaces, they can be tested with mocks. No database or web server is required:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'[TestFixture]\r\npublic class CreateLeaveTypeCommandHandlerTests\r\n{\r\n private readonly CreateLeaveTypeDto _leaveTypeDto;\r\n private readonly CreateLeaveTypeCommandHandler _handler;\r\n\r\n public CreateLeaveTypeCommandHandlerTests()\r\n {\r\n var mock = new Mock>();\r\n var validationMock = new Mock();\r\n\r\n _leaveTypeDto = new CreateLeaveTypeDto\r\n {\r\n DefaultDays = 15,\r\n Name = "Test DTO"\r\n };\r\n\r\n validationMock\r\n .Setup(x => x.ValidateAsync(_leaveTypeDto, false, CancellationToken.None))\r\n .Returns(() => Task.FromResult(new ValidationOutcome()));\r\n\r\n _handler = new CreateLeaveTypeCommandHandler(mock.Object, validationMock.Object);\r\n }\r\n\r\n [Test]\r\n public async Task Valid_LeaveType_Added()\r\n {\r\n var result = await _handler.HandleAsync(\r\n new CreateLeaveTypeCommand { LeaveTypeDto = _leaveTypeDto },\r\n CancellationToken.None);\r\n\r\n result.ShouldBeOfType();\r\n }\r\n}\n'})}),"\n",(0,t.jsx)(n.h2,{id:"key-design-decisions",children:"Key Design Decisions"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsxs)(n.strong,{children:[(0,t.jsx)(n.code,{children:"IGraphRepository"})," as the persistence abstraction."]})," Handlers declare a dependency on ",(0,t.jsx)(n.code,{children:"IGraphRepository"}),". EF Core, NHibernate, or any future provider implements this interface. Swapping providers requires a one-line change in the composition root."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsxs)(n.strong,{children:[(0,t.jsx)(n.code,{children:"DataStoreName"})," for multi-context routing."]})," When an application has multiple databases, handlers set ",(0,t.jsx)(n.code,{children:"repository.DataStoreName"})," to tell RCommon which registered ",(0,t.jsx)(n.code,{children:"DbContext"})," to use. This keeps the routing decision in the handler where it is most visible."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsxs)(n.strong,{children:[(0,t.jsx)(n.code,{children:"AuditableDbContext"})," for automatic audit trails."]})," Inheriting from ",(0,t.jsx)(n.code,{children:"AuditableDbContext"})," removes the need for explicit audit field management across every handler. The ",(0,t.jsx)(n.code,{children:"ICurrentUser"})," and ",(0,t.jsx)(n.code,{children:"ISystemTime"})," services supply the values automatically."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsxs)(n.strong,{children:["Pipeline behaviors via ",(0,t.jsx)(n.code,{children:".AddLoggingToRequestPipeline()"})," and ",(0,t.jsx)(n.code,{children:".AddUnitOfWorkToRequestPipeline()"}),"."]})," These decorators wrap every handler with structured logging and unit-of-work demarcation without modifying the handler code."]}),"\n",(0,t.jsx)(n.h2,{id:"api-reference",children:"API Reference"}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Type"}),(0,t.jsx)(n.th,{children:"Package"}),(0,t.jsx)(n.th,{children:"Purpose"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"AuditedEntity"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"RCommon.Entities"})}),(0,t.jsx)(n.td,{children:"Base class for domain entities with auditing"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Specification"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"RCommon"})}),(0,t.jsx)(n.td,{children:"Composable predicate for domain queries"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"IAppRequest"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"RCommon.Mediator"})}),(0,t.jsx)(n.td,{children:"Marker interface for commands and queries"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"IAppRequestHandler"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"RCommon.Mediator"})}),(0,t.jsx)(n.td,{children:"Handler contract"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"IGraphRepository"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"RCommon.Persistence"})}),(0,t.jsx)(n.td,{children:"Full-featured repository with LINQ support"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"AuditableDbContext"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"RCommon.Persistence.EFCore"})}),(0,t.jsx)(n.td,{children:"EF Core DbContext with automatic audit stamping"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"IMediatorService"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"RCommon.Mediator"})}),(0,t.jsx)(n.td,{children:"Dispatcher used in controllers"})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"IValidationService"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"RCommon.ApplicationServices"})}),(0,t.jsx)(n.td,{children:"Validation abstraction used in handlers"})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}},64996(e,n,r){r.d(n,{A:()=>s});r(30758);var t=r(13526);const a="tabItem_jA3u";var i=r(86070);function s({children:e,hidden:n,className:r}){return(0,i.jsx)("div",{role:"tabpanel",className:(0,t.A)(a,r),hidden:n,children:e})}},85363(e,n,r){r.d(n,{A:()=>j});var t=r(30758),a=r(13526),i=r(32416),s=r(25557),o=r(85924),l=r(64493),c=r(10274),d=r(44118);function u(e){return t.Children.toArray(e).filter(e=>"\n"!==e).map(e=>{if(!e||(0,t.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)})?.filter(Boolean)??[]}function p(e){const{values:n,children:r}=e;return(0,t.useMemo)(()=>{const e=n??function(e){return u(e).map(({props:{value:e,label:n,attributes:r,default:t}})=>({value:e,label:n,attributes:r,default:t}))}(r);return function(e){const n=(0,c.X)(e,(e,n)=>e.value===n.value);if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map(e=>e.value).join(", ")}" found in . Every value needs to be unique.`)}(e),e},[n,r])}function h({value:e,tabValues:n}){return n.some(n=>n.value===e)}function m({queryString:e=!1,groupId:n}){const r=(0,s.W6)(),a=function({queryString:e=!1,groupId:n}){if("string"==typeof e)return e;if(!1===e)return null;if(!0===e&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:e,groupId:n});return[(0,l.aZ)(a),(0,t.useCallback)(e=>{if(!a)return;const n=new URLSearchParams(r.location.search);n.set(a,e),r.replace({...r.location,search:n.toString()})},[a,r])]}function v(e){const{defaultValue:n,queryString:r=!1,groupId:a}=e,i=p(e),[s,l]=(0,t.useState)(()=>function({defaultValue:e,tabValues:n}){if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(e){if(!h({value:e,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${e}" but none of its children has the corresponding value. Available values are: ${n.map(e=>e.value).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return e}const r=n.find(e=>e.default)??n[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:n,tabValues:i})),[c,u]=m({queryString:r,groupId:a}),[v,y]=function({groupId:e}){const n=function(e){return e?`docusaurus.tab.${e}`:null}(e),[r,a]=(0,d.Dv)(n);return[r,(0,t.useCallback)(e=>{n&&a.set(e)},[n,a])]}({groupId:a}),x=(()=>{const e=c??v;return h({value:e,tabValues:i})?e:null})();(0,o.A)(()=>{x&&l(x)},[x]);return{selectedValue:s,selectValue:(0,t.useCallback)(e=>{if(!h({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);l(e),u(e),y(e)},[u,y,i]),tabValues:i}}var y=r(81264);const x="tabList_TfjZ",g="tabItem_vVs9";var T=r(86070);function b({className:e,block:n,selectedValue:r,selectValue:t,tabValues:s}){const o=[],{blockElementScrollPositionUntilNextRender:l}=(0,i.a_)(),c=e=>{const n=e.currentTarget,a=o.indexOf(n),i=s[a].value;i!==r&&(l(n),t(i))},d=e=>{let n=null;switch(e.key){case"Enter":c(e);break;case"ArrowRight":{const r=o.indexOf(e.currentTarget)+1;n=o[r]??o[0];break}case"ArrowLeft":{const r=o.indexOf(e.currentTarget)-1;n=o[r]??o[o.length-1];break}}n?.focus()};return(0,T.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,a.A)("tabs",{"tabs--block":n},e),children:s.map(({value:e,label:n,attributes:t})=>(0,T.jsx)("li",{role:"tab",tabIndex:r===e?0:-1,"aria-selected":r===e,ref:e=>o.push(e),onKeyDown:d,onClick:c,...t,className:(0,a.A)("tabs__item",g,t?.className,{"tabs__item--active":r===e}),children:n??e},e))})}function C({lazy:e,children:n,selectedValue:r}){const a=(Array.isArray(n)?n:[n]).filter(Boolean);if(e){const e=a.find(e=>e.props.value===r);return e?(0,t.cloneElement)(e,{className:"margin-top--md"}):null}return(0,T.jsx)("div",{className:"margin-top--md",children:a.map((e,n)=>(0,t.cloneElement)(e,{key:n,hidden:e.props.value!==r}))})}function f(e){const n=v(e);return(0,T.jsxs)("div",{className:(0,a.A)("tabs-container",x),children:[(0,T.jsx)(b,{...n,...e}),(0,T.jsx)(C,{...n,...e})]})}function j(e){const n=(0,y.A)();return(0,T.jsx)(f,{...e,children:u(e.children)},String(n))}},81753(e,n,r){r.d(n,{R:()=>s,x:()=>o});var t=r(30758);const a={},i=t.createContext(a);function s(e){const n=t.useContext(i);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:s(e.components),t.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/51ddfa24.bb1f37e9.js b/website/build/assets/js/51ddfa24.bb1f37e9.js new file mode 100644 index 00000000..bed46e65 --- /dev/null +++ b/website/build/assets/js/51ddfa24.bb1f37e9.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[4444],{71505(e,n,t){t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>u,frontMatter:()=>r,metadata:()=>o,toc:()=>c});var a=t(86070),i=t(81753);t(85363),t(64996);const r={title:"Clean Architecture",sidebar_position:1,description:"Build .NET apps with Clean Architecture using RCommon. Layer domain, application, and infrastructure with testable CQRS handlers and swappable persistence providers."},s="Clean Architecture with RCommon",o={id:"architecture-guides/clean-architecture",title:"Clean Architecture",description:"Build .NET apps with Clean Architecture using RCommon. Layer domain, application, and infrastructure with testable CQRS handlers and swappable persistence providers.",source:"@site/docs/architecture-guides/clean-architecture.mdx",sourceDirName:"architecture-guides",slug:"/architecture-guides/clean-architecture",permalink:"/docs/next/architecture-guides/clean-architecture",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/architecture-guides/clean-architecture.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Clean Architecture",sidebar_position:1,description:"Build .NET apps with Clean Architecture using RCommon. Layer domain, application, and infrastructure with testable CQRS handlers and swappable persistence providers."},sidebar:"docsSidebar",previous:{title:"Architecture Guides",permalink:"/docs/next/category/architecture-guides"},next:{title:"Microservices",permalink:"/docs/next/architecture-guides/microservices"}},l={},c=[{value:"When to Use This Approach",id:"when-to-use-this-approach",level:2},{value:"Layer Overview",id:"layer-overview",level:2},{value:"Project Structure",id:"project-structure",level:2},{value:"The Domain Layer",id:"the-domain-layer",level:2},{value:"Specifications",id:"specifications",level:3},{value:"The Application Layer",id:"the-application-layer",level:2},{value:"Commands",id:"commands",level:3},{value:"Queries",id:"queries",level:3},{value:"Cross-Cutting Concerns in the Application Layer",id:"cross-cutting-concerns-in-the-application-layer",level:3},{value:"The Infrastructure Layer",id:"the-infrastructure-layer",level:2},{value:"The Composition Root (Program.cs)",id:"the-composition-root-programcs",level:2},{value:"The Presentation Layer",id:"the-presentation-layer",level:2},{value:"Testing Application Handlers",id:"testing-application-handlers",level:2},{value:"Key Design Decisions",id:"key-design-decisions",level:2},{value:"API Reference",id:"api-reference",level:2}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.R)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(n.h1,{id:"clean-architecture-with-rcommon",children:"Clean Architecture with RCommon"}),"\n",(0,a.jsx)(n.p,{children:"Clean Architecture organizes code into concentric layers where the dependency rule states that source code dependencies can only point inward. The domain and application layers have no knowledge of databases, frameworks, or external systems. RCommon supports this structure by providing infrastructure abstractions that your application layer depends on via interfaces, with concrete implementations registered at the composition root."}),"\n",(0,a.jsx)(n.h2,{id:"when-to-use-this-approach",children:"When to Use This Approach"}),"\n",(0,a.jsx)(n.p,{children:"Use Clean Architecture with RCommon when:"}),"\n",(0,a.jsxs)(n.ul,{children:["\n",(0,a.jsx)(n.li,{children:"Your domain logic is complex enough to warrant isolation from infrastructure concerns"}),"\n",(0,a.jsx)(n.li,{children:"You want to swap persistence providers (EF Core to NHibernate, SQL to NoSQL) without rewriting business logic"}),"\n",(0,a.jsx)(n.li,{children:"You need testable handlers that do not depend on a real database or message broker"}),"\n",(0,a.jsx)(n.li,{children:"Multiple teams work on different layers independently"}),"\n"]}),"\n",(0,a.jsx)(n.h2,{id:"layer-overview",children:"Layer Overview"}),"\n",(0,a.jsx)(n.p,{children:"Clean Architecture splits the codebase into four layers. With RCommon, the boundaries map as follows:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{children:"+-------------------------------+\n| Presentation | ASP.NET Core Controllers / MVC\n+-------------------------------+\n| Application | Command/Query Handlers, DTOs, Validation\n+-------------------------------+\n| Domain | Entities, Specifications, Domain Events\n+-------------------------------+\n| Infrastructure | EF Core DbContext, Identity, Email, etc.\n+-------------------------------+\n"})}),"\n",(0,a.jsx)(n.p,{children:"Dependency direction: Presentation -> Application -> Domain. Infrastructure implements the interfaces defined in Application."}),"\n",(0,a.jsx)(n.h2,{id:"project-structure",children:"Project Structure"}),"\n",(0,a.jsx)(n.p,{children:"The HR Leave Management sample included with RCommon demonstrates this layout:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{children:"HR.LeaveManagement.Domain/\n LeaveType.cs\n LeaveRequest.cs\n LeaveAllocation.cs\n Common/BaseDomainEntity.cs\n Specifications/AllocationExistsSpec.cs\n\nHR.LeaveManagement.Application/\n Features/\n LeaveTypes/\n Requests/Commands/CreateLeaveTypeCommand.cs\n Requests/Queries/GetLeaveTypeListRequest.cs\n Handlers/Commands/CreateLeaveTypeCommandHandler.cs\n Handlers/Queries/GetLeaveTypeListRequestHandler.cs\n DTOs/LeaveType/\n Contracts/Identity/IUserService.cs\n ApplicationServicesRegistration.cs\n\nHR.LeaveManagement.Persistence/\n LeaveManagementDbContext.cs\n Configurations/\n\nHR.LeaveManagement.Identity/\n IdentityServicesRegistration.cs\n Services/AuthService.cs\n\nHR.LeaveManagement.API/\n Controllers/LeaveTypesController.cs\n Program.cs\n"})}),"\n",(0,a.jsx)(n.p,{children:"Each project references only what is allowed by the dependency rule. The Domain project has no external references beyond RCommon.Entities. The Application project references Domain but not Persistence or Identity."}),"\n",(0,a.jsx)(n.h2,{id:"the-domain-layer",children:"The Domain Layer"}),"\n",(0,a.jsxs)(n.p,{children:["Domain entities inherit from RCommon's ",(0,a.jsx)(n.code,{children:"AuditedEntity"})," base class, which provides auditing fields (created by, modified by, timestamps) automatically."]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-csharp",children:"// HR.LeaveManagement.Domain/Common/BaseDomainEntity.cs\nusing RCommon.Entities;\n\nnamespace HR.LeaveManagement.Domain.Common\n{\n public abstract class BaseDomainEntity : AuditedEntity\n {\n }\n}\n\n// HR.LeaveManagement.Domain/LeaveType.cs\npublic class LeaveType : BaseDomainEntity\n{\n public string Name { get; set; }\n public int DefaultDays { get; set; }\n}\n\n// HR.LeaveManagement.Domain/LeaveRequest.cs\npublic class LeaveRequest : BaseDomainEntity\n{\n public DateTime StartDate { get; set; }\n public DateTime EndDate { get; set; }\n public LeaveType LeaveType { get; set; }\n public int LeaveTypeId { get; set; }\n public DateTime DateRequested { get; set; }\n public string RequestComments { get; set; }\n public bool? Approved { get; set; }\n public bool Cancelled { get; set; }\n public string RequestingEmployeeId { get; set; }\n}\n"})}),"\n",(0,a.jsx)(n.h3,{id:"specifications",children:"Specifications"}),"\n",(0,a.jsxs)(n.p,{children:["Domain specifications encapsulate business query logic as reusable, composable objects. RCommon's ",(0,a.jsx)(n.code,{children:"Specification"})," provides this:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-csharp",children:"// HR.LeaveManagement.Domain/Specifications/AllocationExistsSpec.cs\nusing RCommon;\n\npublic class AllocationExistsSpec : Specification\n{\n public AllocationExistsSpec(string userId, int leaveTypeId, int period)\n : base(q => q.EmployeeId == userId\n && q.LeaveTypeId == leaveTypeId\n && q.Period == period)\n {\n }\n}\n"})}),"\n",(0,a.jsx)(n.p,{children:"The specification is used in the Application layer without any knowledge of EF Core or SQL:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-csharp",children:"var allocationCount = await _leaveAllocationRepository\n .GetCountAsync(new AllocationExistsSpec(emp.Id, leaveType.Id, period));\n"})}),"\n",(0,a.jsx)(n.h2,{id:"the-application-layer",children:"The Application Layer"}),"\n",(0,a.jsxs)(n.p,{children:["The Application layer contains commands, queries, and their handlers. Handlers implement RCommon's ",(0,a.jsx)(n.code,{children:"IAppRequestHandler"})," interface."]}),"\n",(0,a.jsx)(n.h3,{id:"commands",children:"Commands"}),"\n",(0,a.jsx)(n.p,{children:"A command encapsulates the intent to change state:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-csharp",children:"// CreateLeaveTypeCommand.cs\nusing RCommon.Mediator.Subscribers;\n\npublic class CreateLeaveTypeCommand : IAppRequest\n{\n public CreateLeaveTypeDto LeaveTypeDto { get; set; }\n}\n"})}),"\n",(0,a.jsxs)(n.p,{children:["The handler receives the command, runs validation through ",(0,a.jsx)(n.code,{children:"IValidationService"}),", and persists via the repository interface:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-csharp",children:'// CreateLeaveTypeCommandHandler.cs\npublic class CreateLeaveTypeCommandHandler\n : IAppRequestHandler\n{\n private readonly IGraphRepository _leaveTypeRepository;\n private readonly IValidationService _validationService;\n\n public CreateLeaveTypeCommandHandler(\n IGraphRepository leaveTypeRepository,\n IValidationService validationService)\n {\n _leaveTypeRepository = leaveTypeRepository;\n _validationService = validationService;\n _leaveTypeRepository.DataStoreName = DataStoreNamesConst.LeaveManagement;\n }\n\n public async Task HandleAsync(\n CreateLeaveTypeCommand request,\n CancellationToken cancellationToken)\n {\n var response = new BaseCommandResponse();\n var validationResult = await _validationService.ValidateAsync(request.LeaveTypeDto);\n\n if (!validationResult.IsValid)\n {\n response.Success = false;\n response.Message = "Creation Failed";\n response.Errors = validationResult.Errors\n .Select(q => q.ErrorMessage).ToList();\n }\n else\n {\n var leaveType = request.LeaveTypeDto.ToLeaveType();\n await _leaveTypeRepository.AddAsync(leaveType);\n response.Success = true;\n response.Message = "Creation Successful";\n response.Id = leaveType.Id;\n }\n\n return response;\n }\n}\n'})}),"\n",(0,a.jsx)(n.h3,{id:"queries",children:"Queries"}),"\n",(0,a.jsx)(n.p,{children:"Queries return data without modifying state:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-csharp",children:"// GetLeaveTypeListRequestHandler.cs\npublic class GetLeaveTypeListRequestHandler\n : IAppRequestHandler>\n{\n private readonly IGraphRepository _leaveTypeRepository;\n\n public GetLeaveTypeListRequestHandler(IGraphRepository leaveTypeRepository)\n {\n _leaveTypeRepository = leaveTypeRepository;\n _leaveTypeRepository.DataStoreName = DataStoreNamesConst.LeaveManagement;\n }\n\n public async Task> HandleAsync(\n GetLeaveTypeListRequest request,\n CancellationToken cancellationToken)\n {\n var leaveTypes = await _leaveTypeRepository.FindAsync(x => true);\n return leaveTypes.Select(x => x.ToLeaveTypeDto()).ToList();\n }\n}\n"})}),"\n",(0,a.jsx)(n.h3,{id:"cross-cutting-concerns-in-the-application-layer",children:"Cross-Cutting Concerns in the Application Layer"}),"\n",(0,a.jsx)(n.p,{children:"The Application layer defines contracts for cross-cutting concerns as interfaces. Infrastructure implements them:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-csharp",children:"// Contracts/Identity/IUserService.cs\npublic interface IUserService\n{\n Task> GetEmployees();\n}\n\n// Contracts/Identity/IAuthService.cs\npublic interface IAuthService\n{\n Task Login(AuthRequest request);\n Task Register(RegistrationRequest request);\n}\n"})}),"\n",(0,a.jsx)(n.h2,{id:"the-infrastructure-layer",children:"The Infrastructure Layer"}),"\n",(0,a.jsxs)(n.p,{children:["The ",(0,a.jsx)(n.code,{children:"LeaveManagementDbContext"})," inherits from RCommon's ",(0,a.jsx)(n.code,{children:"AuditableDbContext"}),", which automatically stamps the ",(0,a.jsx)(n.code,{children:"CreatedBy"}),", ",(0,a.jsx)(n.code,{children:"ModifiedBy"}),", ",(0,a.jsx)(n.code,{children:"CreateDate"}),", and ",(0,a.jsx)(n.code,{children:"ModifyDate"})," fields on every save operation:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-csharp",children:"// HR.LeaveManagement.Persistence/LeaveManagementDbContext.cs\npublic class LeaveManagementDbContext : AuditableDbContext\n{\n public LeaveManagementDbContext(\n DbContextOptions options,\n ICurrentUser currentUser,\n ISystemTime systemTime)\n : base(options, currentUser, systemTime)\n {\n }\n\n protected override void OnModelCreating(ModelBuilder modelBuilder)\n {\n modelBuilder.ApplyConfigurationsFromAssembly(\n typeof(LeaveManagementDbContext).Assembly);\n }\n\n public DbSet LeaveRequests { get; set; }\n public DbSet LeaveTypes { get; set; }\n public DbSet LeaveAllocations { get; set; }\n}\n"})}),"\n",(0,a.jsx)(n.h2,{id:"the-composition-root-programcs",children:"The Composition Root (Program.cs)"}),"\n",(0,a.jsxs)(n.p,{children:["All layers are wired together in ",(0,a.jsx)(n.code,{children:"Program.cs"})," using RCommon's fluent builder. This is the only place where concrete implementations are named:"]}),"\n",(0,a.jsx)(n.admonition,{title:"Modular composition",type:"tip",children:(0,a.jsxs)(n.p,{children:["A single ",(0,a.jsx)(n.code,{children:"Program.cs"})," is the easiest pattern, but it is not the only one. RCommon's bootstrapper is cache-aware: each layer (or feature folder) can expose its own static ",(0,a.jsx)(n.code,{children:"Add*ModuleServices(IServiceCollection)"})," method that calls ",(0,a.jsx)(n.code,{children:"services.AddRCommon().With...(...)"})," independently, and the registrations merge predictably. Layer-owned wiring lets the Application layer register handlers and validators while the Infrastructure layer registers the ",(0,a.jsx)(n.code,{children:"DbContext"})," and SendGrid \u2014 without coordinating through one giant ",(0,a.jsx)(n.code,{children:"Program.cs"}),". See ",(0,a.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full contract."]})}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithClaimsAndPrincipalAccessor()\n .WithSendGridEmailServices(x =>\n {\n var sendGridSettings = builder.Configuration.Get();\n x.SendGridApiKey = sendGridSettings.SendGridApiKey;\n x.FromNameDefault = sendGridSettings.FromNameDefault;\n x.FromEmailDefault = sendGridSettings.FromEmailDefault;\n })\n .WithDateTimeSystem(dateTime => dateTime.Kind = DateTimeKind.Utc)\n .WithSequentialGuidGenerator(guid =>\n guid.DefaultSequentialGuidType = SequentialGuidType.SequentialAsString)\n .WithMediator(mediator =>\n {\n mediator.AddRequest();\n mediator.AddRequest,\n GetLeaveTypeListRequestHandler>();\n // ... register all handlers\n\n mediator.Configure(config =>\n {\n config.RegisterServicesFromAssemblies(\n typeof(ApplicationServicesRegistration).GetTypeInfo().Assembly);\n });\n mediator.AddLoggingToRequestPipeline();\n mediator.AddUnitOfWorkToRequestPipeline();\n })\n .WithPersistence(ef =>\n {\n ef.AddDbContext(\n DataStoreNamesConst.LeaveManagement,\n options =>\n {\n options.UseSqlServer(\n builder.Configuration.GetConnectionString(\n DataStoreNamesConst.LeaveManagement));\n });\n ef.SetDefaultDataStore(dataStore =>\n {\n dataStore.DefaultDataStoreName = DataStoreNamesConst.LeaveManagement;\n });\n })\n .WithUnitOfWork(unitOfWork =>\n {\n unitOfWork.SetOptions(options =>\n {\n options.AutoCompleteScope = true;\n options.DefaultIsolation = IsolationLevel.ReadCommitted;\n });\n })\n .WithValidation(validation =>\n {\n validation.AddValidatorsFromAssemblyContaining(\n typeof(ApplicationServicesRegistration));\n });\n"})}),"\n",(0,a.jsx)(n.h2,{id:"the-presentation-layer",children:"The Presentation Layer"}),"\n",(0,a.jsxs)(n.p,{children:["Controllers depend only on ",(0,a.jsx)(n.code,{children:"IMediatorService"}),". They dispatch commands and queries without knowing which handler handles them:"]}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-csharp",children:'[Route("api/[controller]")]\n[ApiController]\n[Authorize]\npublic class LeaveTypesController : ControllerBase\n{\n private readonly IMediatorService _mediator;\n\n public LeaveTypesController(IMediatorService mediator)\n {\n _mediator = mediator;\n }\n\n [HttpGet]\n public async Task>> Get()\n {\n var leaveTypes = await _mediator.Send>(\n new GetLeaveTypeListRequest());\n return Ok(leaveTypes);\n }\n\n [HttpPost]\n [Authorize(Roles = "Administrator")]\n public async Task> Post(\n [FromBody] CreateLeaveTypeDto leaveType)\n {\n var command = new CreateLeaveTypeCommand { LeaveTypeDto = leaveType };\n var response = await _mediator.Send(command);\n return Ok(response);\n }\n}\n'})}),"\n",(0,a.jsx)(n.h2,{id:"testing-application-handlers",children:"Testing Application Handlers"}),"\n",(0,a.jsx)(n.p,{children:"Because handlers depend only on interfaces, they can be tested with mocks. No database or web server is required:"}),"\n",(0,a.jsx)(n.pre,{children:(0,a.jsx)(n.code,{className:"language-csharp",children:'[TestFixture]\npublic class CreateLeaveTypeCommandHandlerTests\n{\n private readonly CreateLeaveTypeDto _leaveTypeDto;\n private readonly CreateLeaveTypeCommandHandler _handler;\n\n public CreateLeaveTypeCommandHandlerTests()\n {\n var mock = new Mock>();\n var validationMock = new Mock();\n\n _leaveTypeDto = new CreateLeaveTypeDto\n {\n DefaultDays = 15,\n Name = "Test DTO"\n };\n\n validationMock\n .Setup(x => x.ValidateAsync(_leaveTypeDto, false, CancellationToken.None))\n .Returns(() => Task.FromResult(new ValidationOutcome()));\n\n _handler = new CreateLeaveTypeCommandHandler(mock.Object, validationMock.Object);\n }\n\n [Test]\n public async Task Valid_LeaveType_Added()\n {\n var result = await _handler.HandleAsync(\n new CreateLeaveTypeCommand { LeaveTypeDto = _leaveTypeDto },\n CancellationToken.None);\n\n result.ShouldBeOfType();\n }\n}\n'})}),"\n",(0,a.jsx)(n.h2,{id:"key-design-decisions",children:"Key Design Decisions"}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsxs)(n.strong,{children:[(0,a.jsx)(n.code,{children:"IGraphRepository"})," as the persistence abstraction."]})," Handlers declare a dependency on ",(0,a.jsx)(n.code,{children:"IGraphRepository"}),". EF Core, NHibernate, or any future provider implements this interface. Swapping providers requires a one-line change in the composition root."]}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsxs)(n.strong,{children:[(0,a.jsx)(n.code,{children:"DataStoreName"})," for multi-context routing."]})," When an application has multiple databases, handlers set ",(0,a.jsx)(n.code,{children:"repository.DataStoreName"})," to tell RCommon which registered ",(0,a.jsx)(n.code,{children:"DbContext"})," to use. This keeps the routing decision in the handler where it is most visible."]}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsxs)(n.strong,{children:[(0,a.jsx)(n.code,{children:"AuditableDbContext"})," for automatic audit trails."]})," Inheriting from ",(0,a.jsx)(n.code,{children:"AuditableDbContext"})," removes the need for explicit audit field management across every handler. The ",(0,a.jsx)(n.code,{children:"ICurrentUser"})," and ",(0,a.jsx)(n.code,{children:"ISystemTime"})," services supply the values automatically."]}),"\n",(0,a.jsxs)(n.p,{children:[(0,a.jsxs)(n.strong,{children:["Pipeline behaviors via ",(0,a.jsx)(n.code,{children:".AddLoggingToRequestPipeline()"})," and ",(0,a.jsx)(n.code,{children:".AddUnitOfWorkToRequestPipeline()"}),"."]})," These decorators wrap every handler with structured logging and unit-of-work demarcation without modifying the handler code."]}),"\n",(0,a.jsx)(n.h2,{id:"api-reference",children:"API Reference"}),"\n",(0,a.jsxs)(n.table,{children:[(0,a.jsx)(n.thead,{children:(0,a.jsxs)(n.tr,{children:[(0,a.jsx)(n.th,{children:"Type"}),(0,a.jsx)(n.th,{children:"Package"}),(0,a.jsx)(n.th,{children:"Purpose"})]})}),(0,a.jsxs)(n.tbody,{children:[(0,a.jsxs)(n.tr,{children:[(0,a.jsx)(n.td,{children:(0,a.jsx)(n.code,{children:"AuditedEntity"})}),(0,a.jsx)(n.td,{children:(0,a.jsx)(n.code,{children:"RCommon.Entities"})}),(0,a.jsx)(n.td,{children:"Base class for domain entities with auditing"})]}),(0,a.jsxs)(n.tr,{children:[(0,a.jsx)(n.td,{children:(0,a.jsx)(n.code,{children:"Specification"})}),(0,a.jsx)(n.td,{children:(0,a.jsx)(n.code,{children:"RCommon"})}),(0,a.jsx)(n.td,{children:"Composable predicate for domain queries"})]}),(0,a.jsxs)(n.tr,{children:[(0,a.jsx)(n.td,{children:(0,a.jsx)(n.code,{children:"IAppRequest"})}),(0,a.jsx)(n.td,{children:(0,a.jsx)(n.code,{children:"RCommon.Mediator"})}),(0,a.jsx)(n.td,{children:"Marker interface for commands and queries"})]}),(0,a.jsxs)(n.tr,{children:[(0,a.jsx)(n.td,{children:(0,a.jsx)(n.code,{children:"IAppRequestHandler"})}),(0,a.jsx)(n.td,{children:(0,a.jsx)(n.code,{children:"RCommon.Mediator"})}),(0,a.jsx)(n.td,{children:"Handler contract"})]}),(0,a.jsxs)(n.tr,{children:[(0,a.jsx)(n.td,{children:(0,a.jsx)(n.code,{children:"IGraphRepository"})}),(0,a.jsx)(n.td,{children:(0,a.jsx)(n.code,{children:"RCommon.Persistence"})}),(0,a.jsx)(n.td,{children:"Full-featured repository with LINQ support"})]}),(0,a.jsxs)(n.tr,{children:[(0,a.jsx)(n.td,{children:(0,a.jsx)(n.code,{children:"AuditableDbContext"})}),(0,a.jsx)(n.td,{children:(0,a.jsx)(n.code,{children:"RCommon.Persistence.EFCore"})}),(0,a.jsx)(n.td,{children:"EF Core DbContext with automatic audit stamping"})]}),(0,a.jsxs)(n.tr,{children:[(0,a.jsx)(n.td,{children:(0,a.jsx)(n.code,{children:"IMediatorService"})}),(0,a.jsx)(n.td,{children:(0,a.jsx)(n.code,{children:"RCommon.Mediator"})}),(0,a.jsx)(n.td,{children:"Dispatcher used in controllers"})]}),(0,a.jsxs)(n.tr,{children:[(0,a.jsx)(n.td,{children:(0,a.jsx)(n.code,{children:"IValidationService"})}),(0,a.jsx)(n.td,{children:(0,a.jsx)(n.code,{children:"RCommon.ApplicationServices"})}),(0,a.jsx)(n.td,{children:"Validation abstraction used in handlers"})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,a.jsx)(n,{...e,children:(0,a.jsx)(d,{...e})}):d(e)}},64996(e,n,t){t.d(n,{A:()=>s});t(30758);var a=t(13526);const i="tabItem_jA3u";var r=t(86070);function s({children:e,hidden:n,className:t}){return(0,r.jsx)("div",{role:"tabpanel",className:(0,a.A)(i,t),hidden:n,children:e})}},85363(e,n,t){t.d(n,{A:()=>j});var a=t(30758),i=t(13526),r=t(32416),s=t(25557),o=t(85924),l=t(64493),c=t(10274),d=t(44118);function u(e){return a.Children.toArray(e).filter(e=>"\n"!==e).map(e=>{if(!e||(0,a.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)})?.filter(Boolean)??[]}function p(e){const{values:n,children:t}=e;return(0,a.useMemo)(()=>{const e=n??function(e){return u(e).map(({props:{value:e,label:n,attributes:t,default:a}})=>({value:e,label:n,attributes:t,default:a}))}(t);return function(e){const n=(0,c.X)(e,(e,n)=>e.value===n.value);if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map(e=>e.value).join(", ")}" found in . Every value needs to be unique.`)}(e),e},[n,t])}function h({value:e,tabValues:n}){return n.some(n=>n.value===e)}function m({queryString:e=!1,groupId:n}){const t=(0,s.W6)(),i=function({queryString:e=!1,groupId:n}){if("string"==typeof e)return e;if(!1===e)return null;if(!0===e&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:e,groupId:n});return[(0,l.aZ)(i),(0,a.useCallback)(e=>{if(!i)return;const n=new URLSearchParams(t.location.search);n.set(i,e),t.replace({...t.location,search:n.toString()})},[i,t])]}function v(e){const{defaultValue:n,queryString:t=!1,groupId:i}=e,r=p(e),[s,l]=(0,a.useState)(()=>function({defaultValue:e,tabValues:n}){if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(e){if(!h({value:e,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${e}" but none of its children has the corresponding value. Available values are: ${n.map(e=>e.value).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return e}const t=n.find(e=>e.default)??n[0];if(!t)throw new Error("Unexpected error: 0 tabValues");return t.value}({defaultValue:n,tabValues:r})),[c,u]=m({queryString:t,groupId:i}),[v,y]=function({groupId:e}){const n=function(e){return e?`docusaurus.tab.${e}`:null}(e),[t,i]=(0,d.Dv)(n);return[t,(0,a.useCallback)(e=>{n&&i.set(e)},[n,i])]}({groupId:i}),x=(()=>{const e=c??v;return h({value:e,tabValues:r})?e:null})();(0,o.A)(()=>{x&&l(x)},[x]);return{selectedValue:s,selectValue:(0,a.useCallback)(e=>{if(!h({value:e,tabValues:r}))throw new Error(`Can't select invalid tab value=${e}`);l(e),u(e),y(e)},[u,y,r]),tabValues:r}}var y=t(81264);const x="tabList_TfjZ",g="tabItem_vVs9";var T=t(86070);function b({className:e,block:n,selectedValue:t,selectValue:a,tabValues:s}){const o=[],{blockElementScrollPositionUntilNextRender:l}=(0,r.a_)(),c=e=>{const n=e.currentTarget,i=o.indexOf(n),r=s[i].value;r!==t&&(l(n),a(r))},d=e=>{let n=null;switch(e.key){case"Enter":c(e);break;case"ArrowRight":{const t=o.indexOf(e.currentTarget)+1;n=o[t]??o[0];break}case"ArrowLeft":{const t=o.indexOf(e.currentTarget)-1;n=o[t]??o[o.length-1];break}}n?.focus()};return(0,T.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,i.A)("tabs",{"tabs--block":n},e),children:s.map(({value:e,label:n,attributes:a})=>(0,T.jsx)("li",{role:"tab",tabIndex:t===e?0:-1,"aria-selected":t===e,ref:e=>o.push(e),onKeyDown:d,onClick:c,...a,className:(0,i.A)("tabs__item",g,a?.className,{"tabs__item--active":t===e}),children:n??e},e))})}function C({lazy:e,children:n,selectedValue:t}){const i=(Array.isArray(n)?n:[n]).filter(Boolean);if(e){const e=i.find(e=>e.props.value===t);return e?(0,a.cloneElement)(e,{className:"margin-top--md"}):null}return(0,T.jsx)("div",{className:"margin-top--md",children:i.map((e,n)=>(0,a.cloneElement)(e,{key:n,hidden:e.props.value!==t}))})}function f(e){const n=v(e);return(0,T.jsxs)("div",{className:(0,i.A)("tabs-container",x),children:[(0,T.jsx)(b,{...n,...e}),(0,T.jsx)(C,{...n,...e})]})}function j(e){const n=(0,y.A)();return(0,T.jsx)(f,{...e,children:u(e.children)},String(n))}},81753(e,n,t){t.d(n,{R:()=>s,x:()=>o});var a=t(30758);const i={},r=a.createContext(i);function s(e){const n=a.useContext(r);return a.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),a.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/523cdb73.28deca8e.js b/website/build/assets/js/523cdb73.28deca8e.js new file mode 100644 index 00000000..b05cea78 --- /dev/null +++ b/website/build/assets/js/523cdb73.28deca8e.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[323],{97930(e,n,t){t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>a,default:()=>h,frontMatter:()=>o,metadata:()=>c,toc:()=>d});var i=t(86070),s=t(81753),r=t(75783);const o={title:"Overview",sidebar_position:1,description:"Multi-tenancy abstraction for RCommon that automatically scopes repository queries and entity stamps to the active tenant via ITenantIdAccessor and IMultiTenant."},a="Multi-Tenancy Overview",c={id:"multi-tenancy/overview",title:"Overview",description:"Multi-tenancy abstraction for RCommon that automatically scopes repository queries and entity stamps to the active tenant via ITenantIdAccessor and IMultiTenant.",source:"@site/docs/multi-tenancy/overview.mdx",sourceDirName:"multi-tenancy",slug:"/multi-tenancy/overview",permalink:"/docs/next/multi-tenancy/overview",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/multi-tenancy/overview.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Overview",sidebar_position:1,description:"Multi-tenancy abstraction for RCommon that automatically scopes repository queries and entity stamps to the active tenant via ITenantIdAccessor and IMultiTenant."},sidebar:"docsSidebar",previous:{title:"Multi-Tenancy",permalink:"/docs/next/category/multi-tenancy"},next:{title:"Finbuckle",permalink:"/docs/next/multi-tenancy/finbuckle"}},l={},d=[{value:"Overview",id:"overview",level:2},{value:"Core abstractions",id:"core-abstractions",level:3},{value:"Entity-level isolation",id:"entity-level-isolation",level:3},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Usage",id:"usage",level:2},{value:"Tenant-scoped entities",id:"tenant-scoped-entities",level:3},{value:"Bypassing tenant isolation",id:"bypassing-tenant-isolation",level:3},{value:"API Summary",id:"api-summary",level:2}];function u(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"multi-tenancy-overview",children:"Multi-Tenancy Overview"}),"\n",(0,i.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsx)(n.p,{children:"Multi-tenancy support in RCommon allows a single application instance to serve multiple tenants while keeping their data isolated. The framework provides a thin abstraction layer over third-party tenant resolution libraries so that your domain and persistence code remains unaware of how tenants are identified."}),"\n",(0,i.jsx)(n.p,{children:"The design is split across two concerns:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Tenant resolution"})," \u2014 determining which tenant is active for the current request. This is delegated to a provider (currently Finbuckle) that inspects the HTTP context, claims, route data, or any other signal you configure."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Tenant filtering"})," \u2014 automatically scoping repository queries and entity stamps to the active tenant. This happens inside RCommon's repository implementations whenever the entity implements ",(0,i.jsx)(n.code,{children:"IMultiTenant"}),"."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"core-abstractions",children:"Core abstractions"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"ITenantIdAccessor"})," is the single point of integration between tenant resolution and the persistence layer. Its ",(0,i.jsx)(n.code,{children:"GetTenantId()"})," method is called by repositories whenever they need to apply tenant isolation. The default registration (",(0,i.jsx)(n.code,{children:"NullTenantIdAccessor"}),") returns ",(0,i.jsx)(n.code,{children:"null"}),", which disables filtering entirely \u2014 useful during bootstrapping or when multi-tenancy is not yet configured."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"IMultiTenantBuilder"})," is the fluent builder interface that all provider-specific builders implement. It gives providers access to the DI service collection so they can replace ",(0,i.jsx)(n.code,{children:"NullTenantIdAccessor"})," with their own implementation."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"WithMultiTenancy"})," is the extension method on ",(0,i.jsx)(n.code,{children:"IRCommonBuilder"})," that wires a provider into the RCommon startup pipeline. It constructs the builder by convention (via ",(0,i.jsx)(n.code,{children:"Activator.CreateInstance"}),") and passes it to the configuration action you supply."]}),"\n",(0,i.jsx)(n.h3,{id:"entity-level-isolation",children:"Entity-level isolation"}),"\n",(0,i.jsxs)(n.p,{children:["Any entity that implements ",(0,i.jsx)(n.code,{children:"IMultiTenant"})," participates in automatic isolation:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Entities;\n\npublic class Invoice : BusinessEntity, IMultiTenant\n{\n public string Number { get; set; } = string.Empty;\n public decimal Amount { get; set; }\n\n // Populated automatically by the repository on add/update.\n public string? TenantId { get; set; }\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["At query time the repository reads the current ",(0,i.jsx)(n.code,{children:"TenantId"})," from ",(0,i.jsx)(n.code,{children:"ITenantIdAccessor"})," and appends a filter expression so only records belonging to that tenant are returned. At write time the same value is stamped onto the entity before it is persisted."]}),"\n",(0,i.jsxs)(n.p,{children:["When ",(0,i.jsx)(n.code,{children:"GetTenantId()"})," returns ",(0,i.jsx)(n.code,{children:"null"})," or an empty string all tenant filtering is bypassed, which is intentional for administrative scenarios where a privileged caller needs cross-tenant access."]}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(r.A,{packageName:"RCommon.MultiTenancy"}),"\n",(0,i.jsx)(n.p,{children:"A provider package is also required. For Finbuckle:"}),"\n",(0,i.jsx)(r.A,{packageName:"RCommon.Finbuckle"}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["Call ",(0,i.jsx)(n.code,{children:"WithMultiTenancy"})," inside your RCommon startup block, passing the concrete builder type for the provider you are using:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'using RCommon;\nusing RCommon.Finbuckle;\nusing Finbuckle.MultiTenant;\n\n// Configure the tenant resolution strategy (Finbuckle\'s own API).\nbuilder.Services.AddMultiTenant()\n .WithHeaderStrategy("X-Tenant-Id")\n .WithConfigurationStore();\n\n// Wire Finbuckle into RCommon.\nbuilder.Services.AddRCommon(config =>\n{\n config\n .WithClaimsAndPrincipalAccessorForWeb()\n .WithPersistence(ef =>\n {\n ef.AddDbContext(\n "App",\n options => options.UseSqlServer(connectionString));\n })\n .WithMultiTenancy>(mt =>\n {\n // FinbuckleTenantIdAccessor is registered automatically.\n // No further configuration is required here unless you need\n // to register custom services against the builder.\n });\n});\n'})}),"\n",(0,i.jsx)(n.admonition,{title:"Modular composition",type:"tip",children:(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"WithMultiTenancy"})," is a cache-aware sub-builder verb. When multiple modules call ",(0,i.jsx)(n.code,{children:"WithMultiTenancy>(...)"})," with the same builder type, the cached builder is reused and each module's configuration action runs against the same instance. ",(0,i.jsx)(n.code,{children:"FinbuckleTenantIdAccessor"})," is registered exactly once. This lets modules independently add tenant-aware services (custom tenant stores, per-tenant options) without coordinating wiring. See ",(0,i.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,i.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(n.h3,{id:"tenant-scoped-entities",children:"Tenant-scoped entities"}),"\n",(0,i.jsxs)(n.p,{children:["Mark entities with ",(0,i.jsx)(n.code,{children:"IMultiTenant"})," to opt into automatic isolation:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class Product : BusinessEntity, IMultiTenant\n{\n public string Name { get; set; } = string.Empty;\n public decimal Price { get; set; }\n\n // Set automatically; do not assign manually in normal usage.\n public string? TenantId { get; set; }\n}\n"})}),"\n",(0,i.jsx)(n.p,{children:"Repository reads are filtered to the current tenant transparently:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"// Returns only the current tenant's products.\nICollection products = await _repository.FindAsync(\n p => p.Price > 0, cancellationToken);\n"})}),"\n",(0,i.jsx)(n.p,{children:"Repository writes stamp the current tenant ID automatically:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'// TenantId is populated from ITenantIdAccessor before INSERT.\nawait _repository.AddAsync(new Product { Name = "Widget", Price = 9.99m }, cancellationToken);\n'})}),"\n",(0,i.jsx)(n.h3,{id:"bypassing-tenant-isolation",children:"Bypassing tenant isolation"}),"\n",(0,i.jsxs)(n.p,{children:["For administrative or cross-tenant operations, resolve ",(0,i.jsx)(n.code,{children:"ITenantIdAccessor"})," from your own infrastructure that returns ",(0,i.jsx)(n.code,{children:"null"}),". Because ",(0,i.jsx)(n.code,{children:"NullTenantIdAccessor"})," is the default when no provider is registered, you can also run without calling ",(0,i.jsx)(n.code,{children:"WithMultiTenancy"})," to keep filtering off globally during development."]}),"\n",(0,i.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Package"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ITenantIdAccessor"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Security"})}),(0,i.jsxs)(n.td,{children:["Returns the current tenant ID; ",(0,i.jsx)(n.code,{children:"null"})," disables all filtering"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"NullTenantIdAccessor"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Security"})}),(0,i.jsxs)(n.td,{children:["Default implementation; always returns ",(0,i.jsx)(n.code,{children:"null"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ClaimsTenantIdAccessor"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Security"})}),(0,i.jsx)(n.td,{children:"Reads tenant ID from the authenticated user's claims"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IMultiTenantBuilder"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.MultiTenancy"})}),(0,i.jsx)(n.td,{children:"Builder interface that provider packages implement"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"MultiTenancyBuilderExtensions"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.MultiTenancy"})}),(0,i.jsxs)(n.td,{children:["Provides ",(0,i.jsx)(n.code,{children:"WithMultiTenancy()"})," on ",(0,i.jsx)(n.code,{children:"IRCommonBuilder"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IMultiTenant"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Entities"})}),(0,i.jsxs)(n.td,{children:["Entity marker interface; exposes ",(0,i.jsx)(n.code,{children:"string? TenantId"})]})]})]})]})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(u,{...e})}):u(e)}},75783(e,n,t){t.d(n,{A:()=>d});var i=t(30758);const s="container_xjrG",r="label_Y4p8",o="commandRow_FY5I",a="command_m7Qs",c="copyButton_u1GK";var l=t(86070);function d({packageName:e,version:n}){const[t,d]=(0,i.useState)(!1),u=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,l.jsxs)("div",{className:s,children:[(0,l.jsx)("div",{className:r,children:"NuGet Package"}),(0,l.jsxs)("div",{className:o,children:[(0,l.jsx)("code",{className:a,children:u}),(0,l.jsx)("button",{className:c,onClick:()=>{navigator.clipboard.writeText(u),d(!0),setTimeout(()=>d(!1),2e3)},title:"Copy to clipboard",children:t?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,t){t.d(n,{R:()=>o,x:()=>a});var i=t(30758);const s={},r=i.createContext(s);function o(e){const n=i.useContext(r);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/523cdb73.8382bb4a.js b/website/build/assets/js/523cdb73.8382bb4a.js deleted file mode 100644 index 38935397..00000000 --- a/website/build/assets/js/523cdb73.8382bb4a.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[323],{97930(e,n,t){t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>a,default:()=>h,frontMatter:()=>o,metadata:()=>c,toc:()=>d});var i=t(86070),r=t(81753),s=t(75783);const o={title:"Overview",sidebar_position:1,description:"Multi-tenancy abstraction for RCommon that automatically scopes repository queries and entity stamps to the active tenant via ITenantIdAccessor and IMultiTenant."},a="Multi-Tenancy Overview",c={id:"multi-tenancy/overview",title:"Overview",description:"Multi-tenancy abstraction for RCommon that automatically scopes repository queries and entity stamps to the active tenant via ITenantIdAccessor and IMultiTenant.",source:"@site/docs/multi-tenancy/overview.mdx",sourceDirName:"multi-tenancy",slug:"/multi-tenancy/overview",permalink:"/docs/next/multi-tenancy/overview",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/multi-tenancy/overview.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Overview",sidebar_position:1,description:"Multi-tenancy abstraction for RCommon that automatically scopes repository queries and entity stamps to the active tenant via ITenantIdAccessor and IMultiTenant."},sidebar:"docsSidebar",previous:{title:"Multi-Tenancy",permalink:"/docs/next/category/multi-tenancy"},next:{title:"Finbuckle",permalink:"/docs/next/multi-tenancy/finbuckle"}},l={},d=[{value:"Overview",id:"overview",level:2},{value:"Core abstractions",id:"core-abstractions",level:3},{value:"Entity-level isolation",id:"entity-level-isolation",level:3},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Usage",id:"usage",level:2},{value:"Tenant-scoped entities",id:"tenant-scoped-entities",level:3},{value:"Bypassing tenant isolation",id:"bypassing-tenant-isolation",level:3},{value:"API Summary",id:"api-summary",level:2}];function u(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"multi-tenancy-overview",children:"Multi-Tenancy Overview"}),"\n",(0,i.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsx)(n.p,{children:"Multi-tenancy support in RCommon allows a single application instance to serve multiple tenants while keeping their data isolated. The framework provides a thin abstraction layer over third-party tenant resolution libraries so that your domain and persistence code remains unaware of how tenants are identified."}),"\n",(0,i.jsx)(n.p,{children:"The design is split across two concerns:"}),"\n",(0,i.jsxs)(n.ul,{children:["\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Tenant resolution"})," \u2014 determining which tenant is active for the current request. This is delegated to a provider (currently Finbuckle) that inspects the HTTP context, claims, route data, or any other signal you configure."]}),"\n",(0,i.jsxs)(n.li,{children:[(0,i.jsx)(n.strong,{children:"Tenant filtering"})," \u2014 automatically scoping repository queries and entity stamps to the active tenant. This happens inside RCommon's repository implementations whenever the entity implements ",(0,i.jsx)(n.code,{children:"IMultiTenant"}),"."]}),"\n"]}),"\n",(0,i.jsx)(n.h3,{id:"core-abstractions",children:"Core abstractions"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"ITenantIdAccessor"})," is the single point of integration between tenant resolution and the persistence layer. Its ",(0,i.jsx)(n.code,{children:"GetTenantId()"})," method is called by repositories whenever they need to apply tenant isolation. The default registration (",(0,i.jsx)(n.code,{children:"NullTenantIdAccessor"}),") returns ",(0,i.jsx)(n.code,{children:"null"}),", which disables filtering entirely \u2014 useful during bootstrapping or when multi-tenancy is not yet configured."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"IMultiTenantBuilder"})," is the fluent builder interface that all provider-specific builders implement. It gives providers access to the DI service collection so they can replace ",(0,i.jsx)(n.code,{children:"NullTenantIdAccessor"})," with their own implementation."]}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"WithMultiTenancy"})," is the extension method on ",(0,i.jsx)(n.code,{children:"IRCommonBuilder"})," that wires a provider into the RCommon startup pipeline. It constructs the builder by convention (via ",(0,i.jsx)(n.code,{children:"Activator.CreateInstance"}),") and passes it to the configuration action you supply."]}),"\n",(0,i.jsx)(n.h3,{id:"entity-level-isolation",children:"Entity-level isolation"}),"\n",(0,i.jsxs)(n.p,{children:["Any entity that implements ",(0,i.jsx)(n.code,{children:"IMultiTenant"})," participates in automatic isolation:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Entities;\r\n\r\npublic class Invoice : BusinessEntity, IMultiTenant\r\n{\r\n public string Number { get; set; } = string.Empty;\r\n public decimal Amount { get; set; }\r\n\r\n // Populated automatically by the repository on add/update.\r\n public string? TenantId { get; set; }\r\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["At query time the repository reads the current ",(0,i.jsx)(n.code,{children:"TenantId"})," from ",(0,i.jsx)(n.code,{children:"ITenantIdAccessor"})," and appends a filter expression so only records belonging to that tenant are returned. At write time the same value is stamped onto the entity before it is persisted."]}),"\n",(0,i.jsxs)(n.p,{children:["When ",(0,i.jsx)(n.code,{children:"GetTenantId()"})," returns ",(0,i.jsx)(n.code,{children:"null"})," or an empty string all tenant filtering is bypassed, which is intentional for administrative scenarios where a privileged caller needs cross-tenant access."]}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(s.A,{packageName:"RCommon.MultiTenancy"}),"\n",(0,i.jsx)(n.p,{children:"A provider package is also required. For Finbuckle:"}),"\n",(0,i.jsx)(s.A,{packageName:"RCommon.Finbuckle"}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["Call ",(0,i.jsx)(n.code,{children:"WithMultiTenancy"})," inside your RCommon startup block, passing the concrete builder type for the provider you are using:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'using RCommon;\r\nusing RCommon.Finbuckle;\r\nusing Finbuckle.MultiTenant;\r\n\r\n// Configure the tenant resolution strategy (Finbuckle\'s own API).\r\nbuilder.Services.AddMultiTenant()\r\n .WithHeaderStrategy("X-Tenant-Id")\r\n .WithConfigurationStore();\r\n\r\n// Wire Finbuckle into RCommon.\r\nbuilder.Services.AddRCommon(config =>\r\n{\r\n config\r\n .WithClaimsAndPrincipalAccessorForWeb()\r\n .WithPersistence(ef =>\r\n {\r\n ef.AddDbContext(\r\n "App",\r\n options => options.UseSqlServer(connectionString));\r\n })\r\n .WithMultiTenancy>(mt =>\r\n {\r\n // FinbuckleTenantIdAccessor is registered automatically.\r\n // No further configuration is required here unless you need\r\n // to register custom services against the builder.\r\n });\r\n});\n'})}),"\n",(0,i.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(n.h3,{id:"tenant-scoped-entities",children:"Tenant-scoped entities"}),"\n",(0,i.jsxs)(n.p,{children:["Mark entities with ",(0,i.jsx)(n.code,{children:"IMultiTenant"})," to opt into automatic isolation:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class Product : BusinessEntity, IMultiTenant\r\n{\r\n public string Name { get; set; } = string.Empty;\r\n public decimal Price { get; set; }\r\n\r\n // Set automatically; do not assign manually in normal usage.\r\n public string? TenantId { get; set; }\r\n}\n"})}),"\n",(0,i.jsx)(n.p,{children:"Repository reads are filtered to the current tenant transparently:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"// Returns only the current tenant's products.\r\nICollection products = await _repository.FindAsync(\r\n p => p.Price > 0, cancellationToken);\n"})}),"\n",(0,i.jsx)(n.p,{children:"Repository writes stamp the current tenant ID automatically:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'// TenantId is populated from ITenantIdAccessor before INSERT.\r\nawait _repository.AddAsync(new Product { Name = "Widget", Price = 9.99m }, cancellationToken);\n'})}),"\n",(0,i.jsx)(n.h3,{id:"bypassing-tenant-isolation",children:"Bypassing tenant isolation"}),"\n",(0,i.jsxs)(n.p,{children:["For administrative or cross-tenant operations, resolve ",(0,i.jsx)(n.code,{children:"ITenantIdAccessor"})," from your own infrastructure that returns ",(0,i.jsx)(n.code,{children:"null"}),". Because ",(0,i.jsx)(n.code,{children:"NullTenantIdAccessor"})," is the default when no provider is registered, you can also run without calling ",(0,i.jsx)(n.code,{children:"WithMultiTenancy"})," to keep filtering off globally during development."]}),"\n",(0,i.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Package"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ITenantIdAccessor"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Security"})}),(0,i.jsxs)(n.td,{children:["Returns the current tenant ID; ",(0,i.jsx)(n.code,{children:"null"})," disables all filtering"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"NullTenantIdAccessor"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Security"})}),(0,i.jsxs)(n.td,{children:["Default implementation; always returns ",(0,i.jsx)(n.code,{children:"null"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ClaimsTenantIdAccessor"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Security"})}),(0,i.jsx)(n.td,{children:"Reads tenant ID from the authenticated user's claims"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IMultiTenantBuilder"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.MultiTenancy"})}),(0,i.jsx)(n.td,{children:"Builder interface that provider packages implement"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"MultiTenancyBuilderExtensions"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.MultiTenancy"})}),(0,i.jsxs)(n.td,{children:["Provides ",(0,i.jsx)(n.code,{children:"WithMultiTenancy()"})," on ",(0,i.jsx)(n.code,{children:"IRCommonBuilder"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IMultiTenant"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RCommon.Entities"})}),(0,i.jsxs)(n.td,{children:["Entity marker interface; exposes ",(0,i.jsx)(n.code,{children:"string? TenantId"})]})]})]})]})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(u,{...e})}):u(e)}},75783(e,n,t){t.d(n,{A:()=>d});var i=t(30758);const r="container_xjrG",s="label_Y4p8",o="commandRow_FY5I",a="command_m7Qs",c="copyButton_u1GK";var l=t(86070);function d({packageName:e,version:n}){const[t,d]=(0,i.useState)(!1),u=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,l.jsxs)("div",{className:r,children:[(0,l.jsx)("div",{className:s,children:"NuGet Package"}),(0,l.jsxs)("div",{className:o,children:[(0,l.jsx)("code",{className:a,children:u}),(0,l.jsx)("button",{className:c,onClick:()=>{navigator.clipboard.writeText(u),d(!0),setTimeout(()=>d(!1),2e3)},title:"Copy to clipboard",children:t?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,t){t.d(n,{R:()=>o,x:()=>a});var i=t(30758);const r={},s=i.createContext(r);function o(e){const n=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/58715ff3.da7f2898.js b/website/build/assets/js/58715ff3.da7f2898.js deleted file mode 100644 index 21546217..00000000 --- a/website/build/assets/js/58715ff3.da7f2898.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[6488],{11560(e,n,r){r.r(n),r.d(n,{assets:()=>o,contentTitle:()=>c,default:()=>h,frontMatter:()=>d,metadata:()=>l,toc:()=>a});var i=r(86070),t=r(81753),s=r(75783);const d={title:"In-Memory Events",sidebar_position:2,description:"Configure RCommon's in-process event bus with InMemoryEventBusBuilder, implement ISubscriber handlers, and publish events via IEventRouter or IEventBus directly."},c="In-Memory Events",l={id:"event-handling/in-memory",title:"In-Memory Events",description:"Configure RCommon's in-process event bus with InMemoryEventBusBuilder, implement ISubscriber handlers, and publish events via IEventRouter or IEventBus directly.",source:"@site/docs/event-handling/in-memory.mdx",sourceDirName:"event-handling",slug:"/event-handling/in-memory",permalink:"/docs/next/event-handling/in-memory",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/event-handling/in-memory.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"In-Memory Events",sidebar_position:2,description:"Configure RCommon's in-process event bus with InMemoryEventBusBuilder, implement ISubscriber handlers, and publish events via IEventRouter or IEventBus directly."},sidebar:"docsSidebar",previous:{title:"Overview",permalink:"/docs/next/event-handling/overview"},next:{title:"Distributed Events",permalink:"/docs/next/event-handling/distributed"}},o={},a=[{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Defining an event",id:"defining-an-event",level:2},{value:"Implementing a subscriber",id:"implementing-a-subscriber",level:2},{value:"Publishing events",id:"publishing-events",level:2},{value:"Via IEventProducer (recommended with unit-of-work)",id:"via-ieventproducer-recommended-with-unit-of-work",level:3},{value:"Via IEventBus (direct, no routing layer)",id:"via-ieventbus-direct-no-routing-layer",level:3},{value:"Dynamic subscriptions",id:"dynamic-subscriptions",level:2},{value:"API summary",id:"api-summary",level:2}];function u(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"in-memory-events",children:"In-Memory Events"}),"\n",(0,i.jsxs)(n.p,{children:["The in-memory event bus provides local publish/subscribe within a single process. Events are dispatched synchronously to all registered ",(0,i.jsx)(n.code,{children:"ISubscriber"})," handlers within the same DI scope. No external infrastructure is required."]}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsxs)(n.p,{children:["The in-memory event bus is part of ",(0,i.jsx)(n.code,{children:"RCommon.Core"}),", which is included when you install the main package:"]}),"\n",(0,i.jsx)(s.A,{packageName:"RCommon.Core"}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["Register the in-memory event bus using ",(0,i.jsx)(n.code,{children:"WithEventHandling"})," on the RCommon builder. Use ",(0,i.jsx)(n.code,{children:"AddProducer"})," to register the producer and ",(0,i.jsx)(n.code,{children:"AddSubscriber"})," to wire each handler to its event type:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\r\nusing RCommon.EventHandling;\r\nusing RCommon.EventHandling.Producers;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithEventHandling(eventHandling =>\r\n {\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n eventHandling.AddSubscriber();\r\n });\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"PublishWithEventBusEventProducer"})," is the built-in producer that delegates to ",(0,i.jsx)(n.code,{children:"IEventBus.PublishAsync"}),". Additional subscribers can be registered for the same event type; all of them will be called."]}),"\n",(0,i.jsx)(n.h2,{id:"defining-an-event",children:"Defining an event"}),"\n",(0,i.jsxs)(n.p,{children:["Events must implement ",(0,i.jsx)(n.code,{children:"ISerializableEvent"}),". Use ",(0,i.jsx)(n.code,{children:"ISyncEvent"})," for sequential dispatch or ",(0,i.jsx)(n.code,{children:"IAsyncEvent"})," for concurrent dispatch:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Models.Events;\r\n\r\npublic class OrderPlaced : ISyncEvent\r\n{\r\n public OrderPlaced(Guid orderId, DateTime placedAt)\r\n {\r\n OrderId = orderId;\r\n PlacedAt = placedAt;\r\n }\r\n\r\n public OrderPlaced() { }\r\n\r\n public Guid OrderId { get; }\r\n public DateTime PlacedAt { get; }\r\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"implementing-a-subscriber",children:"Implementing a subscriber"}),"\n",(0,i.jsxs)(n.p,{children:["Implement ",(0,i.jsx)(n.code,{children:"ISubscriber"})," and register the class in the DI container via ",(0,i.jsx)(n.code,{children:"AddSubscriber"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'using RCommon.EventHandling.Subscribers;\r\n\r\npublic class OrderPlacedHandler : ISubscriber\r\n{\r\n private readonly ILogger _logger;\r\n\r\n public OrderPlacedHandler(ILogger logger)\r\n {\r\n _logger = logger;\r\n }\r\n\r\n public async Task HandleAsync(OrderPlaced @event, CancellationToken cancellationToken = default)\r\n {\r\n _logger.LogInformation("Order {OrderId} placed at {PlacedAt}", @event.OrderId, @event.PlacedAt);\r\n await Task.CompletedTask;\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"publishing-events",children:"Publishing events"}),"\n",(0,i.jsx)(n.h3,{id:"via-ieventproducer-recommended-with-unit-of-work",children:"Via IEventProducer (recommended with unit-of-work)"}),"\n",(0,i.jsxs)(n.p,{children:["Resolve ",(0,i.jsx)(n.code,{children:"IEventProducer"})," or ",(0,i.jsx)(n.code,{children:"IEnumerable"})," and call ",(0,i.jsx)(n.code,{children:"ProduceEventAsync"}),". The router ensures the event reaches only the producers subscribed to it:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class OrderService\r\n{\r\n private readonly IEventRouter _eventRouter;\r\n private readonly IUnitOfWorkFactory _uowFactory;\r\n\r\n public OrderService(IEventRouter eventRouter, IUnitOfWorkFactory uowFactory)\r\n {\r\n _eventRouter = eventRouter;\r\n _uowFactory = uowFactory;\r\n }\r\n\r\n public async Task PlaceOrderAsync(Order order, CancellationToken cancellationToken)\r\n {\r\n using var uow = _uowFactory.CreateUnitOfWork();\r\n // ... persist the order ...\r\n\r\n _eventRouter.AddTransactionalEvent(new OrderPlaced(order.Id, DateTime.UtcNow));\r\n await uow.SaveChangesAsync(cancellationToken);\r\n await _eventRouter.RouteEventsAsync(cancellationToken);\r\n }\r\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"via-ieventbus-direct-no-routing-layer",children:"Via IEventBus (direct, no routing layer)"}),"\n",(0,i.jsxs)(n.p,{children:["You can also publish directly through ",(0,i.jsx)(n.code,{children:"IEventBus"})," without going through the router:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class NotificationService\r\n{\r\n private readonly IEventBus _eventBus;\r\n\r\n public NotificationService(IEventBus eventBus)\r\n {\r\n _eventBus = eventBus;\r\n }\r\n\r\n public async Task NotifyAsync(OrderPlaced @event, CancellationToken cancellationToken)\r\n {\r\n await _eventBus.PublishAsync(@event, cancellationToken);\r\n }\r\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"dynamic-subscriptions",children:"Dynamic subscriptions"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"IEventBus"})," supports runtime subscriptions that do not require DI registration. These are resolved via ",(0,i.jsx)(n.code,{children:"ActivatorUtilities"})," at publish time:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"eventBus.Subscribe();\r\n\r\n// or auto-discover all ISubscriber interfaces on a handler type\r\neventBus.SubscribeAllHandledEvents();\n"})}),"\n",(0,i.jsx)(n.h2,{id:"api-summary",children:"API summary"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InMemoryEventBusBuilder"})}),(0,i.jsxs)(n.td,{children:["Builder used with ",(0,i.jsx)(n.code,{children:"WithEventHandling"})," to configure the in-memory bus"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IEventBus"})}),(0,i.jsxs)(n.td,{children:["In-process event bus; ",(0,i.jsx)(n.code,{children:"PublishAsync"}),", ",(0,i.jsx)(n.code,{children:"Subscribe"}),", ",(0,i.jsx)(n.code,{children:"SubscribeAllHandledEvents"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InMemoryEventBus"})}),(0,i.jsxs)(n.td,{children:["Default ",(0,i.jsx)(n.code,{children:"IEventBus"})," implementation; resolves handlers from a DI scope"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ISubscriber"})}),(0,i.jsxs)(n.td,{children:["Handler interface; implement ",(0,i.jsx)(n.code,{children:"HandleAsync"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"PublishWithEventBusEventProducer"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"IEventProducer"})," that delegates to ",(0,i.jsx)(n.code,{children:"IEventBus.PublishAsync"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IEventRouter"})}),(0,i.jsx)(n.td,{children:"Queues transactional events and routes them to registered producers"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InMemoryTransactionalEventRouter"})}),(0,i.jsxs)(n.td,{children:["Default ",(0,i.jsx)(n.code,{children:"IEventRouter"}),"; registered automatically by ",(0,i.jsx)(n.code,{children:"AddRCommon"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ISyncEvent"})}),(0,i.jsx)(n.td,{children:"Marker; events produced sequentially"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IAsyncEvent"})}),(0,i.jsx)(n.td,{children:"Marker; events produced concurrently"})]})]})]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(u,{...e})}):u(e)}},75783(e,n,r){r.d(n,{A:()=>a});var i=r(30758);const t="container_xjrG",s="label_Y4p8",d="commandRow_FY5I",c="command_m7Qs",l="copyButton_u1GK";var o=r(86070);function a({packageName:e,version:n}){const[r,a]=(0,i.useState)(!1),u=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,o.jsxs)("div",{className:t,children:[(0,o.jsx)("div",{className:s,children:"NuGet Package"}),(0,o.jsxs)("div",{className:d,children:[(0,o.jsx)("code",{className:c,children:u}),(0,o.jsx)("button",{className:l,onClick:()=>{navigator.clipboard.writeText(u),a(!0),setTimeout(()=>a(!1),2e3)},title:"Copy to clipboard",children:r?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,r){r.d(n,{R:()=>d,x:()=>c});var i=r(30758);const t={},s=i.createContext(t);function d(e){const n=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:d(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/58715ff3.e94761a2.js b/website/build/assets/js/58715ff3.e94761a2.js new file mode 100644 index 00000000..9db3e16d --- /dev/null +++ b/website/build/assets/js/58715ff3.e94761a2.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[6488],{11560(e,n,r){r.r(n),r.d(n,{assets:()=>l,contentTitle:()=>c,default:()=>h,frontMatter:()=>d,metadata:()=>o,toc:()=>a});var i=r(86070),t=r(81753),s=r(75783);const d={title:"In-Memory Events",sidebar_position:2,description:"Configure RCommon's in-process event bus with InMemoryEventBusBuilder, implement ISubscriber handlers, and publish events via IEventRouter or IEventBus directly."},c="In-Memory Events",o={id:"event-handling/in-memory",title:"In-Memory Events",description:"Configure RCommon's in-process event bus with InMemoryEventBusBuilder, implement ISubscriber handlers, and publish events via IEventRouter or IEventBus directly.",source:"@site/docs/event-handling/in-memory.mdx",sourceDirName:"event-handling",slug:"/event-handling/in-memory",permalink:"/docs/next/event-handling/in-memory",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/event-handling/in-memory.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"In-Memory Events",sidebar_position:2,description:"Configure RCommon's in-process event bus with InMemoryEventBusBuilder, implement ISubscriber handlers, and publish events via IEventRouter or IEventBus directly."},sidebar:"docsSidebar",previous:{title:"Overview",permalink:"/docs/next/event-handling/overview"},next:{title:"Distributed Events",permalink:"/docs/next/event-handling/distributed"}},l={},a=[{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Defining an event",id:"defining-an-event",level:2},{value:"Implementing a subscriber",id:"implementing-a-subscriber",level:2},{value:"Publishing events",id:"publishing-events",level:2},{value:"Via IEventProducer (recommended with unit-of-work)",id:"via-ieventproducer-recommended-with-unit-of-work",level:3},{value:"Via IEventBus (direct, no routing layer)",id:"via-ieventbus-direct-no-routing-layer",level:3},{value:"Dynamic subscriptions",id:"dynamic-subscriptions",level:2},{value:"API summary",id:"api-summary",level:2}];function u(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"in-memory-events",children:"In-Memory Events"}),"\n",(0,i.jsxs)(n.p,{children:["The in-memory event bus provides local publish/subscribe within a single process. Events are dispatched synchronously to all registered ",(0,i.jsx)(n.code,{children:"ISubscriber"})," handlers within the same DI scope. No external infrastructure is required."]}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsxs)(n.p,{children:["The in-memory event bus is part of ",(0,i.jsx)(n.code,{children:"RCommon.Core"}),", which is included when you install the main package:"]}),"\n",(0,i.jsx)(s.A,{packageName:"RCommon.Core"}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["Register the in-memory event bus using ",(0,i.jsx)(n.code,{children:"WithEventHandling"})," on the RCommon builder. Use ",(0,i.jsx)(n.code,{children:"AddProducer"})," to register the producer and ",(0,i.jsx)(n.code,{children:"AddSubscriber"})," to wire each handler to its event type:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\nusing RCommon.EventHandling;\nusing RCommon.EventHandling.Producers;\n\nbuilder.Services.AddRCommon()\n .WithEventHandling(eventHandling =>\n {\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n eventHandling.AddSubscriber();\n });\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"PublishWithEventBusEventProducer"})," is the built-in producer that delegates to ",(0,i.jsx)(n.code,{children:"IEventBus.PublishAsync"}),". Additional subscribers can be registered for the same event type; all of them will be called."]}),"\n",(0,i.jsx)(n.admonition,{title:"Modular composition",type:"tip",children:(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"WithEventHandling"})," is cache-aware. When multiple modules call it, the cached ",(0,i.jsx)(n.code,{children:"InMemoryEventBusBuilder"})," is reused and each configuration delegate runs against the same instance \u2014 so subscriber and producer registrations from every module accumulate. ",(0,i.jsx)(n.code,{children:"AddProducer"})," deduplicates by concrete producer type: if ",(0,i.jsx)(n.code,{children:"OrderingModule"})," and ",(0,i.jsx)(n.code,{children:"NotificationsModule"})," both call ",(0,i.jsx)(n.code,{children:"eventHandling.AddProducer()"}),", exactly one ",(0,i.jsx)(n.code,{children:"AuditProducer"})," descriptor is registered. ",(0,i.jsx)(n.code,{children:"AddSubscriber"})," accumulates normally, so multiple modules can register handlers for the same event. See ",(0,i.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,i.jsx)(n.h2,{id:"defining-an-event",children:"Defining an event"}),"\n",(0,i.jsxs)(n.p,{children:["Events must implement ",(0,i.jsx)(n.code,{children:"ISerializableEvent"}),". Use ",(0,i.jsx)(n.code,{children:"ISyncEvent"})," for sequential dispatch or ",(0,i.jsx)(n.code,{children:"IAsyncEvent"})," for concurrent dispatch:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Models.Events;\n\npublic class OrderPlaced : ISyncEvent\n{\n public OrderPlaced(Guid orderId, DateTime placedAt)\n {\n OrderId = orderId;\n PlacedAt = placedAt;\n }\n\n public OrderPlaced() { }\n\n public Guid OrderId { get; }\n public DateTime PlacedAt { get; }\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"implementing-a-subscriber",children:"Implementing a subscriber"}),"\n",(0,i.jsxs)(n.p,{children:["Implement ",(0,i.jsx)(n.code,{children:"ISubscriber"})," and register the class in the DI container via ",(0,i.jsx)(n.code,{children:"AddSubscriber"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'using RCommon.EventHandling.Subscribers;\n\npublic class OrderPlacedHandler : ISubscriber\n{\n private readonly ILogger _logger;\n\n public OrderPlacedHandler(ILogger logger)\n {\n _logger = logger;\n }\n\n public async Task HandleAsync(OrderPlaced @event, CancellationToken cancellationToken = default)\n {\n _logger.LogInformation("Order {OrderId} placed at {PlacedAt}", @event.OrderId, @event.PlacedAt);\n await Task.CompletedTask;\n }\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"publishing-events",children:"Publishing events"}),"\n",(0,i.jsx)(n.h3,{id:"via-ieventproducer-recommended-with-unit-of-work",children:"Via IEventProducer (recommended with unit-of-work)"}),"\n",(0,i.jsxs)(n.p,{children:["Resolve ",(0,i.jsx)(n.code,{children:"IEventProducer"})," or ",(0,i.jsx)(n.code,{children:"IEnumerable"})," and call ",(0,i.jsx)(n.code,{children:"ProduceEventAsync"}),". The router ensures the event reaches only the producers subscribed to it:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class OrderService\n{\n private readonly IEventRouter _eventRouter;\n private readonly IUnitOfWorkFactory _uowFactory;\n\n public OrderService(IEventRouter eventRouter, IUnitOfWorkFactory uowFactory)\n {\n _eventRouter = eventRouter;\n _uowFactory = uowFactory;\n }\n\n public async Task PlaceOrderAsync(Order order, CancellationToken cancellationToken)\n {\n using var uow = _uowFactory.CreateUnitOfWork();\n // ... persist the order ...\n\n _eventRouter.AddTransactionalEvent(new OrderPlaced(order.Id, DateTime.UtcNow));\n await uow.SaveChangesAsync(cancellationToken);\n await _eventRouter.RouteEventsAsync(cancellationToken);\n }\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"via-ieventbus-direct-no-routing-layer",children:"Via IEventBus (direct, no routing layer)"}),"\n",(0,i.jsxs)(n.p,{children:["You can also publish directly through ",(0,i.jsx)(n.code,{children:"IEventBus"})," without going through the router:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class NotificationService\n{\n private readonly IEventBus _eventBus;\n\n public NotificationService(IEventBus eventBus)\n {\n _eventBus = eventBus;\n }\n\n public async Task NotifyAsync(OrderPlaced @event, CancellationToken cancellationToken)\n {\n await _eventBus.PublishAsync(@event, cancellationToken);\n }\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"dynamic-subscriptions",children:"Dynamic subscriptions"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"IEventBus"})," supports runtime subscriptions that do not require DI registration. These are resolved via ",(0,i.jsx)(n.code,{children:"ActivatorUtilities"})," at publish time:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"eventBus.Subscribe();\n\n// or auto-discover all ISubscriber interfaces on a handler type\neventBus.SubscribeAllHandledEvents();\n"})}),"\n",(0,i.jsx)(n.h2,{id:"api-summary",children:"API summary"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InMemoryEventBusBuilder"})}),(0,i.jsxs)(n.td,{children:["Builder used with ",(0,i.jsx)(n.code,{children:"WithEventHandling"})," to configure the in-memory bus"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IEventBus"})}),(0,i.jsxs)(n.td,{children:["In-process event bus; ",(0,i.jsx)(n.code,{children:"PublishAsync"}),", ",(0,i.jsx)(n.code,{children:"Subscribe"}),", ",(0,i.jsx)(n.code,{children:"SubscribeAllHandledEvents"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InMemoryEventBus"})}),(0,i.jsxs)(n.td,{children:["Default ",(0,i.jsx)(n.code,{children:"IEventBus"})," implementation; resolves handlers from a DI scope"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ISubscriber"})}),(0,i.jsxs)(n.td,{children:["Handler interface; implement ",(0,i.jsx)(n.code,{children:"HandleAsync"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"PublishWithEventBusEventProducer"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"IEventProducer"})," that delegates to ",(0,i.jsx)(n.code,{children:"IEventBus.PublishAsync"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IEventRouter"})}),(0,i.jsx)(n.td,{children:"Queues transactional events and routes them to registered producers"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InMemoryTransactionalEventRouter"})}),(0,i.jsxs)(n.td,{children:["Default ",(0,i.jsx)(n.code,{children:"IEventRouter"}),"; registered automatically by ",(0,i.jsx)(n.code,{children:"AddRCommon"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ISyncEvent"})}),(0,i.jsx)(n.td,{children:"Marker; events produced sequentially"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IAsyncEvent"})}),(0,i.jsx)(n.td,{children:"Marker; events produced concurrently"})]})]})]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(u,{...e})}):u(e)}},75783(e,n,r){r.d(n,{A:()=>a});var i=r(30758);const t="container_xjrG",s="label_Y4p8",d="commandRow_FY5I",c="command_m7Qs",o="copyButton_u1GK";var l=r(86070);function a({packageName:e,version:n}){const[r,a]=(0,i.useState)(!1),u=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,l.jsxs)("div",{className:t,children:[(0,l.jsx)("div",{className:s,children:"NuGet Package"}),(0,l.jsxs)("div",{className:d,children:[(0,l.jsx)("code",{className:c,children:u}),(0,l.jsx)("button",{className:o,onClick:()=>{navigator.clipboard.writeText(u),a(!0),setTimeout(()=>a(!1),2e3)},title:"Copy to clipboard",children:r?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,r){r.d(n,{R:()=>d,x:()=>c});var i=r(30758);const t={},s=i.createContext(t);function d(e){const n=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:d(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/5a4fc6f7.0fb85829.js b/website/build/assets/js/5a4fc6f7.0fb85829.js new file mode 100644 index 00000000..a8b530c1 --- /dev/null +++ b/website/build/assets/js/5a4fc6f7.0fb85829.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[6581],{62103(e,t,n){n.r(t),n.d(t,{assets:()=>o,contentTitle:()=>c,default:()=>g,frontMatter:()=>s,metadata:()=>d,toc:()=>l});var r=n(86070),i=n(81753),a=n(75783);const s={title:"Stateless",sidebar_position:2,description:"RCommon.Stateless wraps the Stateless library behind IStateMachineConfigurator, supporting guarded transitions, parameterized triggers, and async entry/exit actions."},c="Stateless",d={id:"state-machines/stateless",title:"Stateless",description:"RCommon.Stateless wraps the Stateless library behind IStateMachineConfigurator, supporting guarded transitions, parameterized triggers, and async entry/exit actions.",source:"@site/docs/state-machines/stateless.mdx",sourceDirName:"state-machines",slug:"/state-machines/stateless",permalink:"/docs/next/state-machines/stateless",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/state-machines/stateless.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Stateless",sidebar_position:2,description:"RCommon.Stateless wraps the Stateless library behind IStateMachineConfigurator, supporting guarded transitions, parameterized triggers, and async entry/exit actions."},sidebar:"docsSidebar",previous:{title:"Overview",permalink:"/docs/next/state-machines/overview"},next:{title:"Caching",permalink:"/docs/next/category/caching"}},o={},l=[{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Defining states and triggers",id:"defining-states-and-triggers",level:2},{value:"Configuring a state machine",id:"configuring-a-state-machine",level:2},{value:"How deferred configuration works",id:"how-deferred-configuration-works",level:2},{value:"Guarded transitions",id:"guarded-transitions",level:2},{value:"Firing triggers",id:"firing-triggers",level:2},{value:"Parameterized triggers",id:"parameterized-triggers",level:2},{value:"Entry and exit actions",id:"entry-and-exit-actions",level:2},{value:"Checking permitted triggers",id:"checking-permitted-triggers",level:2},{value:"API summary",id:"api-summary",level:2}];function h(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h1,{id:"stateless",children:"Stateless"}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.code,{children:"RCommon.Stateless"})," wraps the popular ",(0,r.jsx)(t.a,{href:"https://github.com/dotnet-state-machine/stateless",children:"Stateless"})," library behind the RCommon ",(0,r.jsx)(t.code,{children:"IStateMachineConfigurator"})," abstraction. Application code works only with the RCommon interfaces; the Stateless library is an implementation detail."]}),"\n",(0,r.jsx)(t.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(a.A,{packageName:"RCommon.Stateless"}),"\n",(0,r.jsx)(t.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsxs)(t.p,{children:["Register the Stateless adapter with ",(0,r.jsx)(t.code,{children:"WithStatelessStateMachine"})," on the RCommon builder:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-csharp",children:"using RCommon;\n\nbuilder.Services.AddRCommon()\n .WithStatelessStateMachine();\n"})}),"\n",(0,r.jsxs)(t.p,{children:["This registers ",(0,r.jsx)(t.code,{children:"StatelessConfigurator"})," as the open-generic ",(0,r.jsx)(t.code,{children:"IStateMachineConfigurator"})," implementation. Any combination of state and trigger enum types is automatically available for injection."]}),"\n",(0,r.jsx)(t.admonition,{title:"Modular composition",type:"tip",children:(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.code,{children:"WithStatelessStateMachine()"})," is ",(0,r.jsx)(t.code,{children:"TryAdd"}),"-hardened \u2014 repeat calls from multiple modules are idempotent. The open-generic ",(0,r.jsx)(t.code,{children:"IStateMachineConfigurator<,>"})," registration is added exactly once regardless of how many modules invoke this verb. Modules can opt into state-machine support independently without coordinating. See ",(0,r.jsx)(t.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,r.jsx)(t.h2,{id:"defining-states-and-triggers",children:"Defining states and triggers"}),"\n",(0,r.jsxs)(t.p,{children:["States and triggers are plain ",(0,r.jsx)(t.code,{children:"struct"})," enums:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-csharp",children:"public enum OrderState\n{\n Pending,\n Approved,\n Shipped,\n Cancelled\n}\n\npublic enum OrderTrigger\n{\n Approve,\n Ship,\n Cancel\n}\n"})}),"\n",(0,r.jsx)(t.h2,{id:"configuring-a-state-machine",children:"Configuring a state machine"}),"\n",(0,r.jsxs)(t.p,{children:["Inject ",(0,r.jsx)(t.code,{children:"IStateMachineConfigurator"})," and configure each state using ",(0,r.jsx)(t.code,{children:"ForState"}),". Call ",(0,r.jsx)(t.code,{children:"Build"})," to produce a running machine instance:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-csharp",children:'public class OrderStateMachineService\n{\n private readonly IStateMachineConfigurator _configurator;\n\n public OrderStateMachineService(\n IStateMachineConfigurator configurator)\n {\n _configurator = configurator;\n\n _configurator.ForState(OrderState.Pending)\n .Permit(OrderTrigger.Approve, OrderState.Approved)\n .Permit(OrderTrigger.Cancel, OrderState.Cancelled);\n\n _configurator.ForState(OrderState.Approved)\n .Permit(OrderTrigger.Ship, OrderState.Shipped)\n .Permit(OrderTrigger.Cancel, OrderState.Cancelled)\n .OnEntry(async ct =>\n {\n await NotifyWarehouseAsync(ct);\n })\n .OnExit(async ct =>\n {\n await LogStateChangeAsync("leaving Approved", ct);\n });\n\n _configurator.ForState(OrderState.Shipped)\n .OnEntry(async ct =>\n {\n await SendShippingConfirmationAsync(ct);\n });\n }\n\n public IStateMachine BuildFor(OrderState currentState)\n => _configurator.Build(currentState);\n}\n'})}),"\n",(0,r.jsxs)(t.p,{children:["Each call to ",(0,r.jsx)(t.code,{children:"Build"})," produces a fully independent machine instance with its own current state. The configurator can be reused across the lifetime of the application without resetting."]}),"\n",(0,r.jsx)(t.h2,{id:"how-deferred-configuration-works",children:"How deferred configuration works"}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.code,{children:"StatelessConfigurator"})," uses a deferred pattern internally. Calls to ",(0,r.jsx)(t.code,{children:"ForState"})," record configuration actions rather than applying them immediately. When ",(0,r.jsx)(t.code,{children:"Build"})," is called:"]}),"\n",(0,r.jsxs)(t.ol,{children:["\n",(0,r.jsxs)(t.li,{children:["A new ",(0,r.jsx)(t.code,{children:"Stateless.StateMachine"})," is created with the given initial state."]}),"\n",(0,r.jsx)(t.li,{children:"All recorded configuration actions are replayed against the new machine."}),"\n",(0,r.jsxs)(t.li,{children:["The machine is wrapped in ",(0,r.jsx)(t.code,{children:"StatelessStateMachine"})," and returned as ",(0,r.jsx)(t.code,{children:"IStateMachine"}),"."]}),"\n"]}),"\n",(0,r.jsxs)(t.p,{children:["This allows one ",(0,r.jsx)(t.code,{children:"IStateMachineConfigurator"})," instance (registered as transient) to produce many independent machines."]}),"\n",(0,r.jsx)(t.h2,{id:"guarded-transitions",children:"Guarded transitions"}),"\n",(0,r.jsxs)(t.p,{children:["Use ",(0,r.jsx)(t.code,{children:"PermitIf"})," to add a condition to a transition. The guard is evaluated when ",(0,r.jsx)(t.code,{children:"CanFire"})," or ",(0,r.jsx)(t.code,{children:"FireAsync"})," is called:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-csharp",children:"_configurator.ForState(OrderState.Approved)\n .PermitIf(OrderTrigger.Ship, OrderState.Shipped, () => _inventoryService.HasStock())\n .Permit(OrderTrigger.Cancel, OrderState.Cancelled);\n"})}),"\n",(0,r.jsx)(t.h2,{id:"firing-triggers",children:"Firing triggers"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-csharp",children:"var machine = _service.BuildFor(order.State);\n\nif (machine.CanFire(OrderTrigger.Approve))\n{\n await machine.FireAsync(OrderTrigger.Approve, cancellationToken);\n order.State = machine.CurrentState;\n await _repository.UpdateAsync(order, cancellationToken);\n}\n"})}),"\n",(0,r.jsx)(t.h2,{id:"parameterized-triggers",children:"Parameterized triggers"}),"\n",(0,r.jsxs)(t.p,{children:["The Stateless adapter fully supports parameterized triggers. The ",(0,r.jsx)(t.code,{children:"FireAsync"})," overload passes data to the underlying Stateless machine using ",(0,r.jsx)(t.code,{children:"SetTriggerParameters"}),":"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-csharp",children:'// Fire a trigger and pass data to the entry action\nawait machine.FireAsync(OrderTrigger.Ship, new ShipmentDetails { Carrier = "FedEx" }, cancellationToken);\n'})}),"\n",(0,r.jsxs)(t.p,{children:["The trigger parameter descriptor is cached by ",(0,r.jsx)(t.code,{children:"(trigger, dataType)"})," to prevent double-registration, which Stateless does not allow."]}),"\n",(0,r.jsx)(t.h2,{id:"entry-and-exit-actions",children:"Entry and exit actions"}),"\n",(0,r.jsxs)(t.p,{children:["Entry and exit actions are ",(0,r.jsx)(t.code,{children:"Func"}),". Stateless's own ",(0,r.jsx)(t.code,{children:"OnEntryAsync"}),"/",(0,r.jsx)(t.code,{children:"OnExitAsync"})," do not accept a ",(0,r.jsx)(t.code,{children:"CancellationToken"}),"; the adapter passes ",(0,r.jsx)(t.code,{children:"CancellationToken.None"})," internally when forwarding to Stateless. The ",(0,r.jsx)(t.code,{children:"CancellationToken"})," you provide to ",(0,r.jsx)(t.code,{children:"FireAsync"})," is still checked before the trigger fires."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-csharp",children:"_configurator.ForState(OrderState.Approved)\n .OnEntry(async ct =>\n {\n // ct is passed from FireAsync; Stateless itself receives CancellationToken.None\n await _emailService.SendApprovalEmailAsync(ct);\n });\n"})}),"\n",(0,r.jsx)(t.h2,{id:"checking-permitted-triggers",children:"Checking permitted triggers"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-csharp",children:'var machine = _service.BuildFor(order.State);\n\nforeach (var trigger in machine.PermittedTriggers)\n{\n Console.WriteLine($"Can fire: {trigger}");\n}\n'})}),"\n",(0,r.jsx)(t.h2,{id:"api-summary",children:"API summary"}),"\n",(0,r.jsxs)(t.table,{children:[(0,r.jsx)(t.thead,{children:(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.th,{children:"Type"}),(0,r.jsx)(t.th,{children:"Package"}),(0,r.jsx)(t.th,{children:"Description"})]})}),(0,r.jsxs)(t.tbody,{children:[(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"StatelessConfigurator"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"RCommon.Stateless"})}),(0,r.jsxs)(t.td,{children:["Implements ",(0,r.jsx)(t.code,{children:"IStateMachineConfigurator"}),"; records deferred config and builds machines"]})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"StatelessStateMachine"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"RCommon.Stateless"})}),(0,r.jsxs)(t.td,{children:["Wraps ",(0,r.jsx)(t.code,{children:"Stateless.StateMachine"})," as ",(0,r.jsx)(t.code,{children:"IStateMachine"})]})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"DeferredStateConfigurator"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"RCommon.Stateless"})}),(0,r.jsx)(t.td,{children:"Internal class that records state configuration for replay at build time"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"IStateMachineConfigurator"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"RCommon.Core"})}),(0,r.jsx)(t.td,{children:"Abstraction; implement to swap the backing engine"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"IStateMachine"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"RCommon.Core"})}),(0,r.jsx)(t.td,{children:"Running machine abstraction"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"IStateConfigurator"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"RCommon.Core"})}),(0,r.jsx)(t.td,{children:"Per-state transition and action configuration"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"WithStatelessStateMachine()"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"RCommon.Stateless"})}),(0,r.jsxs)(t.td,{children:["Extension method on ",(0,r.jsx)(t.code,{children:"IRCommonBuilder"})]})]})]})]})]})}function g(e={}){const{wrapper:t}={...(0,i.R)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},75783(e,t,n){n.d(t,{A:()=>l});var r=n(30758);const i="container_xjrG",a="label_Y4p8",s="commandRow_FY5I",c="command_m7Qs",d="copyButton_u1GK";var o=n(86070);function l({packageName:e,version:t}){const[n,l]=(0,r.useState)(!1),h=t?`dotnet add package ${e} --version ${t}`:`dotnet add package ${e}`;return(0,o.jsxs)("div",{className:i,children:[(0,o.jsx)("div",{className:a,children:"NuGet Package"}),(0,o.jsxs)("div",{className:s,children:[(0,o.jsx)("code",{className:c,children:h}),(0,o.jsx)("button",{className:d,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:n?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,t,n){n.d(t,{R:()=>s,x:()=>c});var r=n(30758);const i={},a=r.createContext(i);function s(e){const t=r.useContext(a);return r.useMemo(function(){return"function"==typeof e?e(t):{...t,...e}},[t,e])}function c(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),r.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/5a4fc6f7.bdcce958.js b/website/build/assets/js/5a4fc6f7.bdcce958.js deleted file mode 100644 index 1dfdc979..00000000 --- a/website/build/assets/js/5a4fc6f7.bdcce958.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[6581],{62103(e,r,n){n.r(r),n.d(r,{assets:()=>o,contentTitle:()=>c,default:()=>g,frontMatter:()=>s,metadata:()=>d,toc:()=>l});var t=n(86070),i=n(81753),a=n(75783);const s={title:"Stateless",sidebar_position:2,description:"RCommon.Stateless wraps the Stateless library behind IStateMachineConfigurator, supporting guarded transitions, parameterized triggers, and async entry/exit actions."},c="Stateless",d={id:"state-machines/stateless",title:"Stateless",description:"RCommon.Stateless wraps the Stateless library behind IStateMachineConfigurator, supporting guarded transitions, parameterized triggers, and async entry/exit actions.",source:"@site/docs/state-machines/stateless.mdx",sourceDirName:"state-machines",slug:"/state-machines/stateless",permalink:"/docs/next/state-machines/stateless",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/state-machines/stateless.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Stateless",sidebar_position:2,description:"RCommon.Stateless wraps the Stateless library behind IStateMachineConfigurator, supporting guarded transitions, parameterized triggers, and async entry/exit actions."},sidebar:"docsSidebar",previous:{title:"Overview",permalink:"/docs/next/state-machines/overview"},next:{title:"Caching",permalink:"/docs/next/category/caching"}},o={},l=[{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Defining states and triggers",id:"defining-states-and-triggers",level:2},{value:"Configuring a state machine",id:"configuring-a-state-machine",level:2},{value:"How deferred configuration works",id:"how-deferred-configuration-works",level:2},{value:"Guarded transitions",id:"guarded-transitions",level:2},{value:"Firing triggers",id:"firing-triggers",level:2},{value:"Parameterized triggers",id:"parameterized-triggers",level:2},{value:"Entry and exit actions",id:"entry-and-exit-actions",level:2},{value:"Checking permitted triggers",id:"checking-permitted-triggers",level:2},{value:"API summary",id:"api-summary",level:2}];function h(e){const r={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,i.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(r.h1,{id:"stateless",children:"Stateless"}),"\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.code,{children:"RCommon.Stateless"})," wraps the popular ",(0,t.jsx)(r.a,{href:"https://github.com/dotnet-state-machine/stateless",children:"Stateless"})," library behind the RCommon ",(0,t.jsx)(r.code,{children:"IStateMachineConfigurator"})," abstraction. Application code works only with the RCommon interfaces; the Stateless library is an implementation detail."]}),"\n",(0,t.jsx)(r.h2,{id:"installation",children:"Installation"}),"\n",(0,t.jsx)(a.A,{packageName:"RCommon.Stateless"}),"\n",(0,t.jsx)(r.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsxs)(r.p,{children:["Register the Stateless adapter with ",(0,t.jsx)(r.code,{children:"WithStatelessStateMachine"})," on the RCommon builder:"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-csharp",children:"using RCommon;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithStatelessStateMachine();\n"})}),"\n",(0,t.jsxs)(r.p,{children:["This registers ",(0,t.jsx)(r.code,{children:"StatelessConfigurator"})," as the open-generic ",(0,t.jsx)(r.code,{children:"IStateMachineConfigurator"})," implementation. Any combination of state and trigger enum types is automatically available for injection."]}),"\n",(0,t.jsx)(r.h2,{id:"defining-states-and-triggers",children:"Defining states and triggers"}),"\n",(0,t.jsxs)(r.p,{children:["States and triggers are plain ",(0,t.jsx)(r.code,{children:"struct"})," enums:"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-csharp",children:"public enum OrderState\r\n{\r\n Pending,\r\n Approved,\r\n Shipped,\r\n Cancelled\r\n}\r\n\r\npublic enum OrderTrigger\r\n{\r\n Approve,\r\n Ship,\r\n Cancel\r\n}\n"})}),"\n",(0,t.jsx)(r.h2,{id:"configuring-a-state-machine",children:"Configuring a state machine"}),"\n",(0,t.jsxs)(r.p,{children:["Inject ",(0,t.jsx)(r.code,{children:"IStateMachineConfigurator"})," and configure each state using ",(0,t.jsx)(r.code,{children:"ForState"}),". Call ",(0,t.jsx)(r.code,{children:"Build"})," to produce a running machine instance:"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-csharp",children:'public class OrderStateMachineService\r\n{\r\n private readonly IStateMachineConfigurator _configurator;\r\n\r\n public OrderStateMachineService(\r\n IStateMachineConfigurator configurator)\r\n {\r\n _configurator = configurator;\r\n\r\n _configurator.ForState(OrderState.Pending)\r\n .Permit(OrderTrigger.Approve, OrderState.Approved)\r\n .Permit(OrderTrigger.Cancel, OrderState.Cancelled);\r\n\r\n _configurator.ForState(OrderState.Approved)\r\n .Permit(OrderTrigger.Ship, OrderState.Shipped)\r\n .Permit(OrderTrigger.Cancel, OrderState.Cancelled)\r\n .OnEntry(async ct =>\r\n {\r\n await NotifyWarehouseAsync(ct);\r\n })\r\n .OnExit(async ct =>\r\n {\r\n await LogStateChangeAsync("leaving Approved", ct);\r\n });\r\n\r\n _configurator.ForState(OrderState.Shipped)\r\n .OnEntry(async ct =>\r\n {\r\n await SendShippingConfirmationAsync(ct);\r\n });\r\n }\r\n\r\n public IStateMachine BuildFor(OrderState currentState)\r\n => _configurator.Build(currentState);\r\n}\n'})}),"\n",(0,t.jsxs)(r.p,{children:["Each call to ",(0,t.jsx)(r.code,{children:"Build"})," produces a fully independent machine instance with its own current state. The configurator can be reused across the lifetime of the application without resetting."]}),"\n",(0,t.jsx)(r.h2,{id:"how-deferred-configuration-works",children:"How deferred configuration works"}),"\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.code,{children:"StatelessConfigurator"})," uses a deferred pattern internally. Calls to ",(0,t.jsx)(r.code,{children:"ForState"})," record configuration actions rather than applying them immediately. When ",(0,t.jsx)(r.code,{children:"Build"})," is called:"]}),"\n",(0,t.jsxs)(r.ol,{children:["\n",(0,t.jsxs)(r.li,{children:["A new ",(0,t.jsx)(r.code,{children:"Stateless.StateMachine"})," is created with the given initial state."]}),"\n",(0,t.jsx)(r.li,{children:"All recorded configuration actions are replayed against the new machine."}),"\n",(0,t.jsxs)(r.li,{children:["The machine is wrapped in ",(0,t.jsx)(r.code,{children:"StatelessStateMachine"})," and returned as ",(0,t.jsx)(r.code,{children:"IStateMachine"}),"."]}),"\n"]}),"\n",(0,t.jsxs)(r.p,{children:["This allows one ",(0,t.jsx)(r.code,{children:"IStateMachineConfigurator"})," instance (registered as transient) to produce many independent machines."]}),"\n",(0,t.jsx)(r.h2,{id:"guarded-transitions",children:"Guarded transitions"}),"\n",(0,t.jsxs)(r.p,{children:["Use ",(0,t.jsx)(r.code,{children:"PermitIf"})," to add a condition to a transition. The guard is evaluated when ",(0,t.jsx)(r.code,{children:"CanFire"})," or ",(0,t.jsx)(r.code,{children:"FireAsync"})," is called:"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-csharp",children:"_configurator.ForState(OrderState.Approved)\r\n .PermitIf(OrderTrigger.Ship, OrderState.Shipped, () => _inventoryService.HasStock())\r\n .Permit(OrderTrigger.Cancel, OrderState.Cancelled);\n"})}),"\n",(0,t.jsx)(r.h2,{id:"firing-triggers",children:"Firing triggers"}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-csharp",children:"var machine = _service.BuildFor(order.State);\r\n\r\nif (machine.CanFire(OrderTrigger.Approve))\r\n{\r\n await machine.FireAsync(OrderTrigger.Approve, cancellationToken);\r\n order.State = machine.CurrentState;\r\n await _repository.UpdateAsync(order, cancellationToken);\r\n}\n"})}),"\n",(0,t.jsx)(r.h2,{id:"parameterized-triggers",children:"Parameterized triggers"}),"\n",(0,t.jsxs)(r.p,{children:["The Stateless adapter fully supports parameterized triggers. The ",(0,t.jsx)(r.code,{children:"FireAsync"})," overload passes data to the underlying Stateless machine using ",(0,t.jsx)(r.code,{children:"SetTriggerParameters"}),":"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-csharp",children:'// Fire a trigger and pass data to the entry action\r\nawait machine.FireAsync(OrderTrigger.Ship, new ShipmentDetails { Carrier = "FedEx" }, cancellationToken);\n'})}),"\n",(0,t.jsxs)(r.p,{children:["The trigger parameter descriptor is cached by ",(0,t.jsx)(r.code,{children:"(trigger, dataType)"})," to prevent double-registration, which Stateless does not allow."]}),"\n",(0,t.jsx)(r.h2,{id:"entry-and-exit-actions",children:"Entry and exit actions"}),"\n",(0,t.jsxs)(r.p,{children:["Entry and exit actions are ",(0,t.jsx)(r.code,{children:"Func"}),". Stateless's own ",(0,t.jsx)(r.code,{children:"OnEntryAsync"}),"/",(0,t.jsx)(r.code,{children:"OnExitAsync"})," do not accept a ",(0,t.jsx)(r.code,{children:"CancellationToken"}),"; the adapter passes ",(0,t.jsx)(r.code,{children:"CancellationToken.None"})," internally when forwarding to Stateless. The ",(0,t.jsx)(r.code,{children:"CancellationToken"})," you provide to ",(0,t.jsx)(r.code,{children:"FireAsync"})," is still checked before the trigger fires."]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-csharp",children:"_configurator.ForState(OrderState.Approved)\r\n .OnEntry(async ct =>\r\n {\r\n // ct is passed from FireAsync; Stateless itself receives CancellationToken.None\r\n await _emailService.SendApprovalEmailAsync(ct);\r\n });\n"})}),"\n",(0,t.jsx)(r.h2,{id:"checking-permitted-triggers",children:"Checking permitted triggers"}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-csharp",children:'var machine = _service.BuildFor(order.State);\r\n\r\nforeach (var trigger in machine.PermittedTriggers)\r\n{\r\n Console.WriteLine($"Can fire: {trigger}");\r\n}\n'})}),"\n",(0,t.jsx)(r.h2,{id:"api-summary",children:"API summary"}),"\n",(0,t.jsxs)(r.table,{children:[(0,t.jsx)(r.thead,{children:(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.th,{children:"Type"}),(0,t.jsx)(r.th,{children:"Package"}),(0,t.jsx)(r.th,{children:"Description"})]})}),(0,t.jsxs)(r.tbody,{children:[(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"StatelessConfigurator"})}),(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"RCommon.Stateless"})}),(0,t.jsxs)(r.td,{children:["Implements ",(0,t.jsx)(r.code,{children:"IStateMachineConfigurator"}),"; records deferred config and builds machines"]})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"StatelessStateMachine"})}),(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"RCommon.Stateless"})}),(0,t.jsxs)(r.td,{children:["Wraps ",(0,t.jsx)(r.code,{children:"Stateless.StateMachine"})," as ",(0,t.jsx)(r.code,{children:"IStateMachine"})]})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"DeferredStateConfigurator"})}),(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"RCommon.Stateless"})}),(0,t.jsx)(r.td,{children:"Internal class that records state configuration for replay at build time"})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"IStateMachineConfigurator"})}),(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"RCommon.Core"})}),(0,t.jsx)(r.td,{children:"Abstraction; implement to swap the backing engine"})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"IStateMachine"})}),(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"RCommon.Core"})}),(0,t.jsx)(r.td,{children:"Running machine abstraction"})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"IStateConfigurator"})}),(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"RCommon.Core"})}),(0,t.jsx)(r.td,{children:"Per-state transition and action configuration"})]}),(0,t.jsxs)(r.tr,{children:[(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"WithStatelessStateMachine()"})}),(0,t.jsx)(r.td,{children:(0,t.jsx)(r.code,{children:"RCommon.Stateless"})}),(0,t.jsxs)(r.td,{children:["Extension method on ",(0,t.jsx)(r.code,{children:"IRCommonBuilder"})]})]})]})]})]})}function g(e={}){const{wrapper:r}={...(0,i.R)(),...e.components};return r?(0,t.jsx)(r,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},75783(e,r,n){n.d(r,{A:()=>l});var t=n(30758);const i="container_xjrG",a="label_Y4p8",s="commandRow_FY5I",c="command_m7Qs",d="copyButton_u1GK";var o=n(86070);function l({packageName:e,version:r}){const[n,l]=(0,t.useState)(!1),h=r?`dotnet add package ${e} --version ${r}`:`dotnet add package ${e}`;return(0,o.jsxs)("div",{className:i,children:[(0,o.jsx)("div",{className:a,children:"NuGet Package"}),(0,o.jsxs)("div",{className:s,children:[(0,o.jsx)("code",{className:c,children:h}),(0,o.jsx)("button",{className:d,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:n?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,r,n){n.d(r,{R:()=>s,x:()=>c});var t=n(30758);const i={},a=t.createContext(i);function s(e){const r=t.useContext(a);return t.useMemo(function(){return"function"==typeof e?e(r):{...r,...e}},[r,e])}function c(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),t.createElement(a.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/5e6bb026.24e33121.js b/website/build/assets/js/5e6bb026.24e33121.js new file mode 100644 index 00000000..e2bb9b93 --- /dev/null +++ b/website/build/assets/js/5e6bb026.24e33121.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[4103],{30992(e,n,t){t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>p,frontMatter:()=>a,metadata:()=>c,toc:()=>d});var r=t(86070),i=t(81753),o=t(75783);const a={title:"Entity Framework Core",sidebar_position:4,description:"Configure RCommon's EF Core provider to register named DbContexts, inject IGraphRepository with full LINQ support, eager loading, soft delete, and change-tracking control."},s="Entity Framework Core",c={id:"persistence/efcore",title:"Entity Framework Core",description:"Configure RCommon's EF Core provider to register named DbContexts, inject IGraphRepository with full LINQ support, eager loading, soft delete, and change-tracking control.",source:"@site/docs/persistence/efcore.mdx",sourceDirName:"persistence",slug:"/persistence/efcore",permalink:"/docs/next/persistence/efcore",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/persistence/efcore.mdx",tags:[],version:"current",sidebarPosition:4,frontMatter:{title:"Entity Framework Core",sidebar_position:4,description:"Configure RCommon's EF Core provider to register named DbContexts, inject IGraphRepository with full LINQ support, eager loading, soft delete, and change-tracking control."},sidebar:"docsSidebar",previous:{title:"Unit of Work",permalink:"/docs/next/persistence/unit-of-work"},next:{title:"Dapper",permalink:"/docs/next/persistence/dapper"}},l={},d=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"DbContext setup",id:"dbcontext-setup",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Modular composition",id:"modular-composition",level:2},{value:"Usage",id:"usage",level:2},{value:"Injecting and targeting a data store",id:"injecting-and-targeting-a-data-store",level:3},{value:"CRUD operations",id:"crud-operations",level:3},{value:"LINQ queries",id:"linq-queries",level:3},{value:"Eager loading",id:"eager-loading",level:3},{value:"Disabling change tracking",id:"disabling-change-tracking",level:3},{value:"Paged queries",id:"paged-queries",level:3},{value:"Using specifications",id:"using-specifications",level:3},{value:"API Summary",id:"api-summary",level:2}];function h(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"entity-framework-core",children:"Entity Framework Core"}),"\n",(0,r.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,r.jsxs)(n.p,{children:["The EF Core provider wires ",(0,r.jsx)(n.code,{children:"EFCoreRepository"})," into the repository abstraction layer. It implements ",(0,r.jsx)(n.code,{children:"IReadOnlyRepository"}),", ",(0,r.jsx)(n.code,{children:"IWriteOnlyRepository"}),", ",(0,r.jsx)(n.code,{children:"ILinqRepository"}),", and ",(0,r.jsx)(n.code,{children:"IGraphRepository"}),", giving you full LINQ query support, eager loading, automatic soft-delete handling, and opt-in change-tracking control."]}),"\n",(0,r.jsxs)(n.p,{children:["The provider resolves a ",(0,r.jsx)(n.code,{children:"RCommonDbContext"}),"-derived ",(0,r.jsx)(n.code,{children:"DbContext"})," from the ",(0,r.jsx)(n.code,{children:"IDataStoreFactory"})," at runtime, which makes it possible to register multiple named DbContext instances and route repositories to the right one."]}),"\n",(0,r.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(o.A,{packageName:"RCommon.EfCore"}),"\n",(0,r.jsx)(n.h2,{id:"dbcontext-setup",children:"DbContext setup"}),"\n",(0,r.jsxs)(n.p,{children:["Your ",(0,r.jsx)(n.code,{children:"DbContext"})," must derive from ",(0,r.jsx)(n.code,{children:"RCommonDbContext"})," rather than ",(0,r.jsx)(n.code,{children:"DbContext"})," directly. ",(0,r.jsx)(n.code,{children:"RCommonDbContext"})," implements ",(0,r.jsx)(n.code,{children:"IDataStore"}),", which is the mechanism the ",(0,r.jsx)(n.code,{children:"IDataStoreFactory"})," uses to resolve the correct context at runtime."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"using Microsoft.EntityFrameworkCore;\nusing RCommon.Persistence.EFCore;\n\npublic class LeaveManagementDbContext : RCommonDbContext\n{\n public LeaveManagementDbContext(DbContextOptions options)\n : base(options)\n {\n }\n\n protected override void OnModelCreating(ModelBuilder modelBuilder)\n {\n modelBuilder.ApplyConfigurationsFromAssembly(typeof(LeaveManagementDbContext).Assembly);\n }\n\n public DbSet LeaveRequests { get; set; }\n public DbSet LeaveTypes { get; set; }\n public DbSet LeaveAllocations { get; set; }\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["If you need audit stamping or other cross-cutting concerns, introduce an intermediate abstract class that still inherits from ",(0,r.jsx)(n.code,{children:"RCommonDbContext"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'public abstract class AuditableDbContext : RCommonDbContext\n{\n private readonly ICurrentUser _currentUser;\n private readonly ISystemTime _systemTime;\n\n public AuditableDbContext(DbContextOptions options,\n ICurrentUser currentUser, ISystemTime systemTime)\n : base(options)\n {\n _currentUser = currentUser;\n _systemTime = systemTime;\n }\n\n public override Task SaveChangesAsync(\n bool acceptAllChangesOnSuccess,\n CancellationToken cancellationToken = default)\n {\n foreach (var entry in ChangeTracker.Entries()\n .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified))\n {\n string userId = _currentUser?.Id?.ToString() ?? "System";\n entry.Entity.DateLastModified = _systemTime.Now;\n entry.Entity.LastModifiedBy = userId;\n\n if (entry.State == EntityState.Added)\n {\n entry.Entity.DateCreated = _systemTime.Now;\n entry.Entity.CreatedBy = userId;\n }\n }\n\n return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);\n }\n}\n'})}),"\n",(0,r.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsx)(n.p,{children:"Register the EF Core persistence provider in your application startup:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'builder.Services.AddRCommon()\n .WithPersistence(ef =>\n {\n ef.AddDbContext(\n "LeaveManagement",\n options => options.UseSqlServer(\n builder.Configuration.GetConnectionString("LeaveManagement")));\n\n ef.SetDefaultDataStore(ds =>\n ds.DefaultDataStoreName = "LeaveManagement");\n });\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Multiple DbContexts can be registered by calling ",(0,r.jsx)(n.code,{children:"AddDbContext"})," multiple times with different names:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'ef.AddDbContext("Inventory",\n options => options.UseNpgsql(inventoryConnectionString));\n\nef.AddDbContext("Ordering",\n options => options.UseSqlServer(orderingConnectionString));\n\nef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "Ordering");\n'})}),"\n",(0,r.jsx)(n.h2,{id:"modular-composition",children:"Modular composition"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"WithPersistence"})," is cache-aware. When two modules both call it, the second call reuses the cached ",(0,r.jsx)(n.code,{children:"EFCorePerisistenceBuilder"})," rather than constructing a new one \u2014 both configuration delegates run against the same instance, so each module can declare its own DbContexts side by side:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'// In OrderingModule.Configure\nservices.AddRCommon()\n .WithPersistence(ef =>\n ef.AddDbContext("Ordering",\n o => o.UseSqlServer(orderingConn)));\n\n// In InventoryModule.Configure\nservices.AddRCommon()\n .WithPersistence(ef =>\n ef.AddDbContext("Inventory",\n o => o.UseNpgsql(inventoryConn)));\n'})}),"\n",(0,r.jsxs)(n.p,{children:["After both modules run, ",(0,r.jsx)(n.code,{children:"IDataStoreFactory"})," resolves ",(0,r.jsx)(n.code,{children:'"Ordering"'})," and ",(0,r.jsx)(n.code,{children:'"Inventory"'})," to their respective contexts. The ",(0,r.jsx)(n.code,{children:"EFCorePerisistenceBuilder"})," itself is constructed exactly once."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"AddDbContext(name, ...)"})," has the following conflict semantics across modules:"]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Re-registration shape"}),(0,r.jsx)(n.th,{children:"Behaviour"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["Same ",(0,r.jsx)(n.code,{children:"name"})," + same ",(0,r.jsx)(n.code,{children:"TDbContext"})," (same options-builder allowed to differ)"]}),(0,r.jsx)(n.td,{children:"Idempotent \u2014 the duplicate is dropped."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["Same ",(0,r.jsx)(n.code,{children:"name"})," + different ",(0,r.jsx)(n.code,{children:"TDbContext"})]}),(0,r.jsxs)(n.td,{children:["Throws ",(0,r.jsx)(n.code,{children:"UnsupportedDataStoreException"})," naming both context types."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsxs)(n.td,{children:["Different ",(0,r.jsx)(n.code,{children:"name"})," + same or different ",(0,r.jsx)(n.code,{children:"TDbContext"})]}),(0,r.jsx)(n.td,{children:"Both are registered."})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:["See ",(0,r.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]}),"\n",(0,r.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,r.jsx)(n.h3,{id:"injecting-and-targeting-a-data-store",children:"Injecting and targeting a data store"}),"\n",(0,r.jsxs)(n.p,{children:["Inject ",(0,r.jsx)(n.code,{children:"IGraphRepository"})," (or any narrower interface) and set ",(0,r.jsx)(n.code,{children:"DataStoreName"})," to match the name given to ",(0,r.jsx)(n.code,{children:"AddDbContext"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'public class CreateLeaveTypeCommandHandler\n{\n private readonly IGraphRepository _leaveTypeRepository;\n\n public CreateLeaveTypeCommandHandler(IGraphRepository leaveTypeRepository)\n {\n _leaveTypeRepository = leaveTypeRepository;\n _leaveTypeRepository.DataStoreName = "LeaveManagement";\n }\n\n public async Task HandleAsync(CreateLeaveTypeCommand request, CancellationToken cancellationToken)\n {\n var leaveType = new LeaveType { Name = request.Name, DefaultDays = request.DefaultDays };\n await _leaveTypeRepository.AddAsync(leaveType, cancellationToken);\n }\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"crud-operations",children:"CRUD operations"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'// Create\nawait _repo.AddAsync(entity, cancellationToken);\nawait _repo.AddRangeAsync(entities, cancellationToken);\n\n// Read\nLeaveType? lt = await _repo.FindAsync(id, cancellationToken);\nvar all = await _repo.FindAsync(lt => lt.DefaultDays > 0, cancellationToken);\nvar single = await _repo.FindSingleOrDefaultAsync(lt => lt.Name == "Annual", cancellationToken);\nbool any = await _repo.AnyAsync(lt => lt.DefaultDays > 20, cancellationToken);\nlong count = await _repo.GetCountAsync(lt => lt.DefaultDays > 0, cancellationToken);\n\n// Update\nawait _repo.UpdateAsync(entity, cancellationToken);\n\n// Delete (auto-detects ISoftDelete)\nawait _repo.DeleteAsync(entity, cancellationToken);\n\n// Bulk delete\nint affected = await _repo.DeleteManyAsync(lt => lt.DefaultDays == 0, cancellationToken);\n'})}),"\n",(0,r.jsx)(n.h3,{id:"linq-queries",children:"LINQ queries"}),"\n",(0,r.jsxs)(n.p,{children:["Because ",(0,r.jsx)(n.code,{children:"IGraphRepository"})," extends ",(0,r.jsx)(n.code,{children:"IQueryable"})," you can use it directly in LINQ expressions:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"IQueryable query = _repo.FindQuery(lt => lt.DefaultDays > 5);\n"})}),"\n",(0,r.jsx)(n.h3,{id:"eager-loading",children:"Eager loading"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"var requests = await _leaveRequestRepo\n .Include(r => r.LeaveType)\n .FindAsync(r => r.EmployeeId == employeeId, cancellationToken);\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Chain additional navigation properties with ",(0,r.jsx)(n.code,{children:"ThenInclude"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"var allocations = await _allocationRepo\n .Include(a => a.LeaveType)\n .FindAsync(a => a.EmployeeId == userId, cancellationToken);\n"})}),"\n",(0,r.jsx)(n.h3,{id:"disabling-change-tracking",children:"Disabling change tracking"}),"\n",(0,r.jsxs)(n.p,{children:["Set ",(0,r.jsx)(n.code,{children:"Tracking = false"})," for read-only queries that do not need to participate in the change tracker:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"_repo.Tracking = false;\nvar list = await _repo.FindAsync(lt => lt.DefaultDays > 0, cancellationToken);\n"})}),"\n",(0,r.jsx)(n.h3,{id:"paged-queries",children:"Paged queries"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"IPaginatedList page = await _repo.FindAsync(\n expression: lt => lt.DefaultDays > 0,\n orderByExpression: lt => lt.Name,\n orderByAscending: true,\n pageNumber: 1,\n pageSize: 10,\n token: cancellationToken);\n"})}),"\n",(0,r.jsx)(n.h3,{id:"using-specifications",children:"Using specifications"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"var spec = new AllocationExistsSpec(userId, leaveTypeId, year);\nlong count = await _allocationRepo.GetCountAsync(spec, cancellationToken);\n"})}),"\n",(0,r.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Purpose"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"IEFCorePersistenceBuilder"})}),(0,r.jsxs)(n.td,{children:["Fluent startup builder with ",(0,r.jsx)(n.code,{children:"AddDbContext"})," and ",(0,r.jsx)(n.code,{children:"SetDefaultDataStore"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"EFCorePerisistenceBuilder"})}),(0,r.jsx)(n.td,{children:"Concrete implementation that registers EF Core repositories in the DI container"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommonDbContext"})}),(0,r.jsxs)(n.td,{children:["Abstract base class for all EF Core contexts; implements ",(0,r.jsx)(n.code,{children:"IDataStore"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"EFCoreRepository"})}),(0,r.jsxs)(n.td,{children:["Concrete repository; implements ",(0,r.jsx)(n.code,{children:"IGraphRepository"})," and lower interfaces"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"IGraphRepository"})}),(0,r.jsxs)(n.td,{children:["Full CRUD + LINQ + paging + eager loading + ",(0,r.jsx)(n.code,{children:"Tracking"})," property"]})]})]})]})]})}function p(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},75783(e,n,t){t.d(n,{A:()=>d});var r=t(30758);const i="container_xjrG",o="label_Y4p8",a="commandRow_FY5I",s="command_m7Qs",c="copyButton_u1GK";var l=t(86070);function d({packageName:e,version:n}){const[t,d]=(0,r.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,l.jsxs)("div",{className:i,children:[(0,l.jsx)("div",{className:o,children:"NuGet Package"}),(0,l.jsxs)("div",{className:a,children:[(0,l.jsx)("code",{className:s,children:h}),(0,l.jsx)("button",{className:c,onClick:()=>{navigator.clipboard.writeText(h),d(!0),setTimeout(()=>d(!1),2e3)},title:"Copy to clipboard",children:t?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,t){t.d(n,{R:()=>a,x:()=>s});var r=t(30758);const i={},o=r.createContext(i);function a(e){const n=r.useContext(o);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),r.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/5e6bb026.70347693.js b/website/build/assets/js/5e6bb026.70347693.js deleted file mode 100644 index 77e8a410..00000000 --- a/website/build/assets/js/5e6bb026.70347693.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[4103],{30992(e,n,t){t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>s,default:()=>h,frontMatter:()=>o,metadata:()=>c,toc:()=>d});var r=t(86070),a=t(81753),i=t(75783);const o={title:"Entity Framework Core",sidebar_position:4,description:"Configure RCommon's EF Core provider to register named DbContexts, inject IGraphRepository with full LINQ support, eager loading, soft delete, and change-tracking control."},s="Entity Framework Core",c={id:"persistence/efcore",title:"Entity Framework Core",description:"Configure RCommon's EF Core provider to register named DbContexts, inject IGraphRepository with full LINQ support, eager loading, soft delete, and change-tracking control.",source:"@site/docs/persistence/efcore.mdx",sourceDirName:"persistence",slug:"/persistence/efcore",permalink:"/docs/next/persistence/efcore",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/persistence/efcore.mdx",tags:[],version:"current",sidebarPosition:4,frontMatter:{title:"Entity Framework Core",sidebar_position:4,description:"Configure RCommon's EF Core provider to register named DbContexts, inject IGraphRepository with full LINQ support, eager loading, soft delete, and change-tracking control."},sidebar:"docsSidebar",previous:{title:"Unit of Work",permalink:"/docs/next/persistence/unit-of-work"},next:{title:"Dapper",permalink:"/docs/next/persistence/dapper"}},l={},d=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"DbContext setup",id:"dbcontext-setup",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Usage",id:"usage",level:2},{value:"Injecting and targeting a data store",id:"injecting-and-targeting-a-data-store",level:3},{value:"CRUD operations",id:"crud-operations",level:3},{value:"LINQ queries",id:"linq-queries",level:3},{value:"Eager loading",id:"eager-loading",level:3},{value:"Disabling change tracking",id:"disabling-change-tracking",level:3},{value:"Paged queries",id:"paged-queries",level:3},{value:"Using specifications",id:"using-specifications",level:3},{value:"API Summary",id:"api-summary",level:2}];function p(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,a.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"entity-framework-core",children:"Entity Framework Core"}),"\n",(0,r.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,r.jsxs)(n.p,{children:["The EF Core provider wires ",(0,r.jsx)(n.code,{children:"EFCoreRepository"})," into the repository abstraction layer. It implements ",(0,r.jsx)(n.code,{children:"IReadOnlyRepository"}),", ",(0,r.jsx)(n.code,{children:"IWriteOnlyRepository"}),", ",(0,r.jsx)(n.code,{children:"ILinqRepository"}),", and ",(0,r.jsx)(n.code,{children:"IGraphRepository"}),", giving you full LINQ query support, eager loading, automatic soft-delete handling, and opt-in change-tracking control."]}),"\n",(0,r.jsxs)(n.p,{children:["The provider resolves a ",(0,r.jsx)(n.code,{children:"RCommonDbContext"}),"-derived ",(0,r.jsx)(n.code,{children:"DbContext"})," from the ",(0,r.jsx)(n.code,{children:"IDataStoreFactory"})," at runtime, which makes it possible to register multiple named DbContext instances and route repositories to the right one."]}),"\n",(0,r.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(i.A,{packageName:"RCommon.EfCore"}),"\n",(0,r.jsx)(n.h2,{id:"dbcontext-setup",children:"DbContext setup"}),"\n",(0,r.jsxs)(n.p,{children:["Your ",(0,r.jsx)(n.code,{children:"DbContext"})," must derive from ",(0,r.jsx)(n.code,{children:"RCommonDbContext"})," rather than ",(0,r.jsx)(n.code,{children:"DbContext"})," directly. ",(0,r.jsx)(n.code,{children:"RCommonDbContext"})," implements ",(0,r.jsx)(n.code,{children:"IDataStore"}),", which is the mechanism the ",(0,r.jsx)(n.code,{children:"IDataStoreFactory"})," uses to resolve the correct context at runtime."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"using Microsoft.EntityFrameworkCore;\r\nusing RCommon.Persistence.EFCore;\r\n\r\npublic class LeaveManagementDbContext : RCommonDbContext\r\n{\r\n public LeaveManagementDbContext(DbContextOptions options)\r\n : base(options)\r\n {\r\n }\r\n\r\n protected override void OnModelCreating(ModelBuilder modelBuilder)\r\n {\r\n modelBuilder.ApplyConfigurationsFromAssembly(typeof(LeaveManagementDbContext).Assembly);\r\n }\r\n\r\n public DbSet LeaveRequests { get; set; }\r\n public DbSet LeaveTypes { get; set; }\r\n public DbSet LeaveAllocations { get; set; }\r\n}\n"})}),"\n",(0,r.jsxs)(n.p,{children:["If you need audit stamping or other cross-cutting concerns, introduce an intermediate abstract class that still inherits from ",(0,r.jsx)(n.code,{children:"RCommonDbContext"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'public abstract class AuditableDbContext : RCommonDbContext\r\n{\r\n private readonly ICurrentUser _currentUser;\r\n private readonly ISystemTime _systemTime;\r\n\r\n public AuditableDbContext(DbContextOptions options,\r\n ICurrentUser currentUser, ISystemTime systemTime)\r\n : base(options)\r\n {\r\n _currentUser = currentUser;\r\n _systemTime = systemTime;\r\n }\r\n\r\n public override Task SaveChangesAsync(\r\n bool acceptAllChangesOnSuccess,\r\n CancellationToken cancellationToken = default)\r\n {\r\n foreach (var entry in ChangeTracker.Entries()\r\n .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified))\r\n {\r\n string userId = _currentUser?.Id?.ToString() ?? "System";\r\n entry.Entity.DateLastModified = _systemTime.Now;\r\n entry.Entity.LastModifiedBy = userId;\r\n\r\n if (entry.State == EntityState.Added)\r\n {\r\n entry.Entity.DateCreated = _systemTime.Now;\r\n entry.Entity.CreatedBy = userId;\r\n }\r\n }\r\n\r\n return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);\r\n }\r\n}\n'})}),"\n",(0,r.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsx)(n.p,{children:"Register the EF Core persistence provider in your application startup:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'builder.Services.AddRCommon()\r\n .WithPersistence(ef =>\r\n {\r\n ef.AddDbContext(\r\n "LeaveManagement",\r\n options => options.UseSqlServer(\r\n builder.Configuration.GetConnectionString("LeaveManagement")));\r\n\r\n ef.SetDefaultDataStore(ds =>\r\n ds.DefaultDataStoreName = "LeaveManagement");\r\n });\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Multiple DbContexts can be registered by calling ",(0,r.jsx)(n.code,{children:"AddDbContext"})," multiple times with different names:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'ef.AddDbContext("Inventory",\r\n options => options.UseNpgsql(inventoryConnectionString));\r\n\r\nef.AddDbContext("Ordering",\r\n options => options.UseSqlServer(orderingConnectionString));\r\n\r\nef.SetDefaultDataStore(ds => ds.DefaultDataStoreName = "Ordering");\n'})}),"\n",(0,r.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,r.jsx)(n.h3,{id:"injecting-and-targeting-a-data-store",children:"Injecting and targeting a data store"}),"\n",(0,r.jsxs)(n.p,{children:["Inject ",(0,r.jsx)(n.code,{children:"IGraphRepository"})," (or any narrower interface) and set ",(0,r.jsx)(n.code,{children:"DataStoreName"})," to match the name given to ",(0,r.jsx)(n.code,{children:"AddDbContext"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'public class CreateLeaveTypeCommandHandler\r\n{\r\n private readonly IGraphRepository _leaveTypeRepository;\r\n\r\n public CreateLeaveTypeCommandHandler(IGraphRepository leaveTypeRepository)\r\n {\r\n _leaveTypeRepository = leaveTypeRepository;\r\n _leaveTypeRepository.DataStoreName = "LeaveManagement";\r\n }\r\n\r\n public async Task HandleAsync(CreateLeaveTypeCommand request, CancellationToken cancellationToken)\r\n {\r\n var leaveType = new LeaveType { Name = request.Name, DefaultDays = request.DefaultDays };\r\n await _leaveTypeRepository.AddAsync(leaveType, cancellationToken);\r\n }\r\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"crud-operations",children:"CRUD operations"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'// Create\r\nawait _repo.AddAsync(entity, cancellationToken);\r\nawait _repo.AddRangeAsync(entities, cancellationToken);\r\n\r\n// Read\r\nLeaveType? lt = await _repo.FindAsync(id, cancellationToken);\r\nvar all = await _repo.FindAsync(lt => lt.DefaultDays > 0, cancellationToken);\r\nvar single = await _repo.FindSingleOrDefaultAsync(lt => lt.Name == "Annual", cancellationToken);\r\nbool any = await _repo.AnyAsync(lt => lt.DefaultDays > 20, cancellationToken);\r\nlong count = await _repo.GetCountAsync(lt => lt.DefaultDays > 0, cancellationToken);\r\n\r\n// Update\r\nawait _repo.UpdateAsync(entity, cancellationToken);\r\n\r\n// Delete (auto-detects ISoftDelete)\r\nawait _repo.DeleteAsync(entity, cancellationToken);\r\n\r\n// Bulk delete\r\nint affected = await _repo.DeleteManyAsync(lt => lt.DefaultDays == 0, cancellationToken);\n'})}),"\n",(0,r.jsx)(n.h3,{id:"linq-queries",children:"LINQ queries"}),"\n",(0,r.jsxs)(n.p,{children:["Because ",(0,r.jsx)(n.code,{children:"IGraphRepository"})," extends ",(0,r.jsx)(n.code,{children:"IQueryable"})," you can use it directly in LINQ expressions:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"IQueryable query = _repo.FindQuery(lt => lt.DefaultDays > 5);\n"})}),"\n",(0,r.jsx)(n.h3,{id:"eager-loading",children:"Eager loading"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"var requests = await _leaveRequestRepo\r\n .Include(r => r.LeaveType)\r\n .FindAsync(r => r.EmployeeId == employeeId, cancellationToken);\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Chain additional navigation properties with ",(0,r.jsx)(n.code,{children:"ThenInclude"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"var allocations = await _allocationRepo\r\n .Include(a => a.LeaveType)\r\n .FindAsync(a => a.EmployeeId == userId, cancellationToken);\n"})}),"\n",(0,r.jsx)(n.h3,{id:"disabling-change-tracking",children:"Disabling change tracking"}),"\n",(0,r.jsxs)(n.p,{children:["Set ",(0,r.jsx)(n.code,{children:"Tracking = false"})," for read-only queries that do not need to participate in the change tracker:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"_repo.Tracking = false;\r\nvar list = await _repo.FindAsync(lt => lt.DefaultDays > 0, cancellationToken);\n"})}),"\n",(0,r.jsx)(n.h3,{id:"paged-queries",children:"Paged queries"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"IPaginatedList page = await _repo.FindAsync(\r\n expression: lt => lt.DefaultDays > 0,\r\n orderByExpression: lt => lt.Name,\r\n orderByAscending: true,\r\n pageNumber: 1,\r\n pageSize: 10,\r\n token: cancellationToken);\n"})}),"\n",(0,r.jsx)(n.h3,{id:"using-specifications",children:"Using specifications"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"var spec = new AllocationExistsSpec(userId, leaveTypeId, year);\r\nlong count = await _allocationRepo.GetCountAsync(spec, cancellationToken);\n"})}),"\n",(0,r.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Purpose"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"IEFCorePersistenceBuilder"})}),(0,r.jsxs)(n.td,{children:["Fluent startup builder with ",(0,r.jsx)(n.code,{children:"AddDbContext"})," and ",(0,r.jsx)(n.code,{children:"SetDefaultDataStore"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"EFCorePerisistenceBuilder"})}),(0,r.jsx)(n.td,{children:"Concrete implementation that registers EF Core repositories in the DI container"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommonDbContext"})}),(0,r.jsxs)(n.td,{children:["Abstract base class for all EF Core contexts; implements ",(0,r.jsx)(n.code,{children:"IDataStore"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"EFCoreRepository"})}),(0,r.jsxs)(n.td,{children:["Concrete repository; implements ",(0,r.jsx)(n.code,{children:"IGraphRepository"})," and lower interfaces"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"IGraphRepository"})}),(0,r.jsxs)(n.td,{children:["Full CRUD + LINQ + paging + eager loading + ",(0,r.jsx)(n.code,{children:"Tracking"})," property"]})]})]})]})]})}function h(e={}){const{wrapper:n}={...(0,a.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(p,{...e})}):p(e)}},75783(e,n,t){t.d(n,{A:()=>d});var r=t(30758);const a="container_xjrG",i="label_Y4p8",o="commandRow_FY5I",s="command_m7Qs",c="copyButton_u1GK";var l=t(86070);function d({packageName:e,version:n}){const[t,d]=(0,r.useState)(!1),p=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,l.jsxs)("div",{className:a,children:[(0,l.jsx)("div",{className:i,children:"NuGet Package"}),(0,l.jsxs)("div",{className:o,children:[(0,l.jsx)("code",{className:s,children:p}),(0,l.jsx)("button",{className:c,onClick:()=>{navigator.clipboard.writeText(p),d(!0),setTimeout(()=>d(!1),2e3)},title:"Copy to clipboard",children:t?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,t){t.d(n,{R:()=>o,x:()=>s});var r=t(30758);const a={},i=r.createContext(a);function o(e){const n=r.useContext(i);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:o(e.components),r.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/5f236a12.a910bf04.js b/website/build/assets/js/5f236a12.a910bf04.js deleted file mode 100644 index 8182690c..00000000 --- a/website/build/assets/js/5f236a12.a910bf04.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[5493],{59425(e,i,n){n.r(i),n.d(i,{assets:()=>a,contentTitle:()=>o,default:()=>x,frontMatter:()=>d,metadata:()=>t,toc:()=>l});var c=n(86070),s=n(81753),r=n(75783);const d={title:"Redis Cache",sidebar_position:3,description:"RCommon.RedisCache implements ICacheService using StackExchange.Redis for distributed caching across multiple app instances, with JSON serialization support."},o="Redis Cache",t={id:"caching/redis",title:"Redis Cache",description:"RCommon.RedisCache implements ICacheService using StackExchange.Redis for distributed caching across multiple app instances, with JSON serialization support.",source:"@site/docs/caching/redis.mdx",sourceDirName:"caching",slug:"/caching/redis",permalink:"/docs/next/caching/redis",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/caching/redis.mdx",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"Redis Cache",sidebar_position:3,description:"RCommon.RedisCache implements ICacheService using StackExchange.Redis for distributed caching across multiple app instances, with JSON serialization support."},sidebar:"docsSidebar",previous:{title:"Memory Cache",permalink:"/docs/next/caching/memory"},next:{title:"Blob Storage",permalink:"/docs/next/category/blob-storage"}},a={},l=[{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Minimal setup",id:"minimal-setup",level:3},{value:"Connection configuration",id:"connection-configuration",level:2},{value:"Expression caching",id:"expression-caching",level:2},{value:"Using ICacheService",id:"using-icacheservice",level:2},{value:"How RedisCacheService works",id:"how-rediscacheservice-works",level:2},{value:"JSON serialization dependency",id:"json-serialization-dependency",level:2},{value:"API summary",id:"api-summary",level:2}];function h(e){const i={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)(i.h1,{id:"redis-cache",children:"Redis Cache"}),"\n",(0,c.jsxs)(i.p,{children:[(0,c.jsx)(i.code,{children:"RCommon.RedisCache"})," implements ",(0,c.jsx)(i.code,{children:"ICacheService"})," using StackExchange.Redis through the ",(0,c.jsx)(i.code,{children:"Microsoft.Extensions.Caching.StackExchangeRedis"})," provider. It is the recommended caching backend for applications deployed across multiple instances or hosts."]}),"\n",(0,c.jsx)(i.h2,{id:"installation",children:"Installation"}),"\n",(0,c.jsx)(r.A,{packageName:"RCommon.RedisCache"}),"\n",(0,c.jsx)(i.h2,{id:"configuration",children:"Configuration"}),"\n",(0,c.jsxs)(i.p,{children:["Register Redis caching with ",(0,c.jsx)(i.code,{children:"WithDistributedCaching"}),":"]}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:'using RCommon;\r\nusing RCommon.RedisCache;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithDistributedCaching(cache =>\r\n {\r\n cache.Configure(options =>\r\n {\r\n options.Configuration = "localhost:6379";\r\n options.InstanceName = "MyApp:";\r\n });\r\n });\n'})}),"\n",(0,c.jsxs)(i.p,{children:[(0,c.jsx)(i.code,{children:"Configure"})," calls ",(0,c.jsx)(i.code,{children:"AddStackExchangeRedisCache"})," on the underlying ",(0,c.jsx)(i.code,{children:"IServiceCollection"}),", registering ",(0,c.jsx)(i.code,{children:"IDistributedCache"})," with the Redis provider. ",(0,c.jsx)(i.code,{children:"RedisCacheService"})," is then available as ",(0,c.jsx)(i.code,{children:"ICacheService"}),"."]}),"\n",(0,c.jsx)(i.h3,{id:"minimal-setup",children:"Minimal setup"}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:'builder.Services.AddRCommon()\r\n .WithDistributedCaching(cache =>\r\n {\r\n cache.Configure(options =>\r\n {\r\n options.Configuration = "localhost:6379";\r\n });\r\n });\n'})}),"\n",(0,c.jsx)(i.h2,{id:"connection-configuration",children:"Connection configuration"}),"\n",(0,c.jsxs)(i.p,{children:["The ",(0,c.jsx)(i.code,{children:"RedisCacheOptions"})," object (from ",(0,c.jsx)(i.code,{children:"Microsoft.Extensions.Caching.StackExchangeRedis"}),") exposes the following commonly used settings:"]}),"\n",(0,c.jsxs)(i.table,{children:[(0,c.jsx)(i.thead,{children:(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.th,{children:"Option"}),(0,c.jsx)(i.th,{children:"Description"})]})}),(0,c.jsxs)(i.tbody,{children:[(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"Configuration"})}),(0,c.jsxs)(i.td,{children:["Redis connection string, e.g. ",(0,c.jsx)(i.code,{children:'"localhost:6379"'})," or a full StackExchange.Redis connection string"]})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"InstanceName"})}),(0,c.jsxs)(i.td,{children:["Optional key prefix applied to all cache keys, e.g. ",(0,c.jsx)(i.code,{children:'"MyApp:"'})]})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"ConfigurationOptions"})}),(0,c.jsxs)(i.td,{children:["Full ",(0,c.jsx)(i.code,{children:"StackExchange.Redis.ConfigurationOptions"})," for advanced scenarios (SSL, auth, retry policy)"]})]})]})]}),"\n",(0,c.jsx)(i.p,{children:"For production use with authentication and TLS:"}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:'cache.Configure(options =>\r\n{\r\n options.ConfigurationOptions = new StackExchange.Redis.ConfigurationOptions\r\n {\r\n EndPoints = { "my-redis.cache.windows.net:6380" },\r\n Password = "your-access-key",\r\n Ssl = true,\r\n AbortOnConnectFail = false\r\n };\r\n options.InstanceName = "MyApp:";\r\n});\n'})}),"\n",(0,c.jsx)(i.h2,{id:"expression-caching",children:"Expression caching"}),"\n",(0,c.jsxs)(i.p,{children:["Enable RCommon's internal expression caching optimization by calling ",(0,c.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions"}),":"]}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:'builder.Services.AddRCommon()\r\n .WithDistributedCaching(cache =>\r\n {\r\n cache.Configure(options =>\r\n {\r\n options.Configuration = "localhost:6379";\r\n });\r\n cache.CacheDynamicallyCompiledExpressions();\r\n });\n'})}),"\n",(0,c.jsx)(i.p,{children:"This registers:"}),"\n",(0,c.jsxs)(i.ul,{children:["\n",(0,c.jsxs)(i.li,{children:[(0,c.jsx)(i.code,{children:"ICacheService"})," as ",(0,c.jsx)(i.code,{children:"RedisCacheService"})]}),"\n",(0,c.jsxs)(i.li,{children:[(0,c.jsx)(i.code,{children:"CachingOptions"})," with ",(0,c.jsx)(i.code,{children:"CachingEnabled = true"})," and ",(0,c.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions = true"})]}),"\n",(0,c.jsxs)(i.li,{children:["A ",(0,c.jsx)(i.code,{children:"Func"})," factory for strategy-based resolution"]}),"\n"]}),"\n",(0,c.jsxs)(i.p,{children:["Note: the MassTransit documentation recommends using in-memory caching for expression caching because Redis introduces network overhead. Consider using ",(0,c.jsx)(i.code,{children:"RCommon.MemoryCache"})," for expression caching alongside Redis for application-level caching if performance is critical."]}),"\n",(0,c.jsx)(i.h2,{id:"using-icacheservice",children:"Using ICacheService"}),"\n",(0,c.jsxs)(i.p,{children:["Inject ",(0,c.jsx)(i.code,{children:"ICacheService"})," and use the get-or-create pattern. ",(0,c.jsx)(i.code,{children:"RedisCacheService"})," serializes values to JSON using ",(0,c.jsx)(i.code,{children:"IJsonSerializer"})," before storing them in Redis:"]}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:'public class ProductCatalogService\r\n{\r\n private readonly ICacheService _cache;\r\n private readonly IProductRepository _repository;\r\n\r\n public ProductCatalogService(ICacheService cache, IProductRepository repository)\r\n {\r\n _cache = cache;\r\n _repository = repository;\r\n }\r\n\r\n public async Task> GetActiveProductsAsync(CancellationToken ct)\r\n {\r\n var key = CacheKey.With(typeof(ProductCatalogService), "active-products");\r\n\r\n return await _cache.GetOrCreateAsync(key, () =>\r\n {\r\n // This runs only on a cache miss\r\n return _repository.FindAsync(p => p.IsActive).Result;\r\n });\r\n }\r\n}\n'})}),"\n",(0,c.jsx)(i.h2,{id:"how-rediscacheservice-works",children:"How RedisCacheService works"}),"\n",(0,c.jsxs)(i.p,{children:[(0,c.jsx)(i.code,{children:"RedisCacheService"})," implements the get-or-create pattern against ",(0,c.jsx)(i.code,{children:"IDistributedCache"}),":"]}),"\n",(0,c.jsxs)(i.ol,{children:["\n",(0,c.jsxs)(i.li,{children:["The key is converted to a string via ",(0,c.jsx)(i.code,{children:".ToString()"}),"."]}),"\n",(0,c.jsxs)(i.li,{children:[(0,c.jsx)(i.code,{children:"GetStringAsync(key)"})," is called. If the result is non-null, the JSON string is deserialized and returned."]}),"\n",(0,c.jsxs)(i.li,{children:["On a cache miss, the factory delegate is invoked, the result is serialized to JSON via ",(0,c.jsx)(i.code,{children:"IJsonSerializer"}),", stored with ",(0,c.jsx)(i.code,{children:"SetStringAsync"}),", and returned."]}),"\n"]}),"\n",(0,c.jsxs)(i.p,{children:["There is no built-in expiration set at the ",(0,c.jsx)(i.code,{children:"ICacheService"})," level; all entries are stored without a TTL by default. For expiration control, wrap ",(0,c.jsx)(i.code,{children:"IDistributedCache"})," directly and set ",(0,c.jsx)(i.code,{children:"DistributedCacheEntryOptions"}),"."]}),"\n",(0,c.jsx)(i.h2,{id:"json-serialization-dependency",children:"JSON serialization dependency"}),"\n",(0,c.jsxs)(i.p,{children:[(0,c.jsx)(i.code,{children:"RedisCacheService"})," depends on ",(0,c.jsx)(i.code,{children:"IJsonSerializer"})," from ",(0,c.jsx)(i.code,{children:"RCommon.Core"}),". Ensure a JSON serialization provider is registered before configuring Redis caching:"]}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:'builder.Services.AddRCommon()\r\n .WithJsonSerialization() // or NewtonsoftJsonSerializer\r\n .WithDistributedCaching(cache =>\r\n {\r\n cache.Configure(options =>\r\n {\r\n options.Configuration = "localhost:6379";\r\n });\r\n });\n'})}),"\n",(0,c.jsx)(i.h2,{id:"api-summary",children:"API summary"}),"\n",(0,c.jsxs)(i.table,{children:[(0,c.jsx)(i.thead,{children:(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.th,{children:"Type"}),(0,c.jsx)(i.th,{children:"Package"}),(0,c.jsx)(i.th,{children:"Description"})]})}),(0,c.jsxs)(i.tbody,{children:[(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RedisCachingBuilder"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.RedisCache"})}),(0,c.jsx)(i.td,{children:"Concrete builder for Redis-backed distributed caching"})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"IRedisCachingBuilder"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.RedisCache"})}),(0,c.jsxs)(i.td,{children:["Marker interface extending ",(0,c.jsx)(i.code,{children:"IDistributedCachingBuilder"})]})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RedisCacheService"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.RedisCache"})}),(0,c.jsxs)(i.td,{children:[(0,c.jsx)(i.code,{children:"ICacheService"})," implementation backed by ",(0,c.jsx)(i.code,{children:"IDistributedCache"})," (Redis) with JSON serialization"]})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"Configure(options)"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.RedisCache"})}),(0,c.jsxs)(i.td,{children:["Extension on ",(0,c.jsx)(i.code,{children:"IRedisCachingBuilder"}),"; configures ",(0,c.jsx)(i.code,{children:"RedisCacheOptions"})," via ",(0,c.jsx)(i.code,{children:"AddStackExchangeRedisCache"})]})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions()"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.RedisCache"})}),(0,c.jsxs)(i.td,{children:["Extension enabling expression caching optimization using ",(0,c.jsx)(i.code,{children:"RedisCacheService"})]})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"ICacheService"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.Caching"})}),(0,c.jsx)(i.td,{children:"Core abstraction for get-or-create caching"})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"CacheKey"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.Caching"})}),(0,c.jsx)(i.td,{children:"Strongly-typed cache key with factory methods"})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"WithDistributedCaching()"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.Caching"})}),(0,c.jsxs)(i.td,{children:["Extension method on ",(0,c.jsx)(i.code,{children:"IRCommonBuilder"})]})]})]})]})]})}function x(e={}){const{wrapper:i}={...(0,s.R)(),...e.components};return i?(0,c.jsx)(i,{...e,children:(0,c.jsx)(h,{...e})}):h(e)}},75783(e,i,n){n.d(i,{A:()=>l});var c=n(30758);const s="container_xjrG",r="label_Y4p8",d="commandRow_FY5I",o="command_m7Qs",t="copyButton_u1GK";var a=n(86070);function l({packageName:e,version:i}){const[n,l]=(0,c.useState)(!1),h=i?`dotnet add package ${e} --version ${i}`:`dotnet add package ${e}`;return(0,a.jsxs)("div",{className:s,children:[(0,a.jsx)("div",{className:r,children:"NuGet Package"}),(0,a.jsxs)("div",{className:d,children:[(0,a.jsx)("code",{className:o,children:h}),(0,a.jsx)("button",{className:t,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:n?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,i,n){n.d(i,{R:()=>d,x:()=>o});var c=n(30758);const s={},r=c.createContext(s);function d(e){const i=c.useContext(r);return c.useMemo(function(){return"function"==typeof e?e(i):{...i,...e}},[i,e])}function o(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:d(e.components),c.createElement(r.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/5f236a12.dfa49e9c.js b/website/build/assets/js/5f236a12.dfa49e9c.js new file mode 100644 index 00000000..1337d557 --- /dev/null +++ b/website/build/assets/js/5f236a12.dfa49e9c.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[5493],{59425(e,i,n){n.r(i),n.d(i,{assets:()=>a,contentTitle:()=>o,default:()=>x,frontMatter:()=>d,metadata:()=>t,toc:()=>l});var c=n(86070),s=n(81753),r=n(75783);const d={title:"Redis Cache",sidebar_position:3,description:"RCommon.RedisCache implements ICacheService using StackExchange.Redis for distributed caching across multiple app instances, with JSON serialization support."},o="Redis Cache",t={id:"caching/redis",title:"Redis Cache",description:"RCommon.RedisCache implements ICacheService using StackExchange.Redis for distributed caching across multiple app instances, with JSON serialization support.",source:"@site/docs/caching/redis.mdx",sourceDirName:"caching",slug:"/caching/redis",permalink:"/docs/next/caching/redis",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/caching/redis.mdx",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"Redis Cache",sidebar_position:3,description:"RCommon.RedisCache implements ICacheService using StackExchange.Redis for distributed caching across multiple app instances, with JSON serialization support."},sidebar:"docsSidebar",previous:{title:"Memory Cache",permalink:"/docs/next/caching/memory"},next:{title:"Blob Storage",permalink:"/docs/next/category/blob-storage"}},a={},l=[{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Minimal setup",id:"minimal-setup",level:3},{value:"Connection configuration",id:"connection-configuration",level:2},{value:"Expression caching",id:"expression-caching",level:2},{value:"Using ICacheService",id:"using-icacheservice",level:2},{value:"How RedisCacheService works",id:"how-rediscacheservice-works",level:2},{value:"JSON serialization dependency",id:"json-serialization-dependency",level:2},{value:"API summary",id:"api-summary",level:2}];function h(e){const i={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)(i.h1,{id:"redis-cache",children:"Redis Cache"}),"\n",(0,c.jsxs)(i.p,{children:[(0,c.jsx)(i.code,{children:"RCommon.RedisCache"})," implements ",(0,c.jsx)(i.code,{children:"ICacheService"})," using StackExchange.Redis through the ",(0,c.jsx)(i.code,{children:"Microsoft.Extensions.Caching.StackExchangeRedis"})," provider. It is the recommended caching backend for applications deployed across multiple instances or hosts."]}),"\n",(0,c.jsx)(i.h2,{id:"installation",children:"Installation"}),"\n",(0,c.jsx)(r.A,{packageName:"RCommon.RedisCache"}),"\n",(0,c.jsx)(i.h2,{id:"configuration",children:"Configuration"}),"\n",(0,c.jsxs)(i.p,{children:["Register Redis caching with ",(0,c.jsx)(i.code,{children:"WithDistributedCaching"}),":"]}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:'using RCommon;\nusing RCommon.RedisCache;\n\nbuilder.Services.AddRCommon()\n .WithDistributedCaching(cache =>\n {\n cache.Configure(options =>\n {\n options.Configuration = "localhost:6379";\n options.InstanceName = "MyApp:";\n });\n });\n'})}),"\n",(0,c.jsxs)(i.p,{children:[(0,c.jsx)(i.code,{children:"Configure"})," calls ",(0,c.jsx)(i.code,{children:"AddStackExchangeRedisCache"})," on the underlying ",(0,c.jsx)(i.code,{children:"IServiceCollection"}),", registering ",(0,c.jsx)(i.code,{children:"IDistributedCache"})," with the Redis provider. ",(0,c.jsx)(i.code,{children:"RedisCacheService"})," is then available as ",(0,c.jsx)(i.code,{children:"ICacheService"}),"."]}),"\n",(0,c.jsx)(i.admonition,{title:"Modular composition",type:"tip",children:(0,c.jsxs)(i.p,{children:[(0,c.jsx)(i.code,{children:"WithDistributedCaching"})," is a cache-aware sub-builder verb. When multiple modules call it, the cached ",(0,c.jsx)(i.code,{children:"RedisCachingBuilder"})," is reused and each module's ",(0,c.jsx)(i.code,{children:"Configure(...)"})," and ",(0,c.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions()"})," calls accumulate on the same instance. Configuration delegates run in the order modules invoke them, so the last ",(0,c.jsx)(i.code,{children:"Configure(...)"})," wins for ",(0,c.jsx)(i.code,{children:"RedisCacheOptions"})," values that are simple scalar assignments \u2014 coordinate connection-string ownership in a single module. See ",(0,c.jsx)(i.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,c.jsx)(i.h3,{id:"minimal-setup",children:"Minimal setup"}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:'builder.Services.AddRCommon()\n .WithDistributedCaching(cache =>\n {\n cache.Configure(options =>\n {\n options.Configuration = "localhost:6379";\n });\n });\n'})}),"\n",(0,c.jsx)(i.h2,{id:"connection-configuration",children:"Connection configuration"}),"\n",(0,c.jsxs)(i.p,{children:["The ",(0,c.jsx)(i.code,{children:"RedisCacheOptions"})," object (from ",(0,c.jsx)(i.code,{children:"Microsoft.Extensions.Caching.StackExchangeRedis"}),") exposes the following commonly used settings:"]}),"\n",(0,c.jsxs)(i.table,{children:[(0,c.jsx)(i.thead,{children:(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.th,{children:"Option"}),(0,c.jsx)(i.th,{children:"Description"})]})}),(0,c.jsxs)(i.tbody,{children:[(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"Configuration"})}),(0,c.jsxs)(i.td,{children:["Redis connection string, e.g. ",(0,c.jsx)(i.code,{children:'"localhost:6379"'})," or a full StackExchange.Redis connection string"]})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"InstanceName"})}),(0,c.jsxs)(i.td,{children:["Optional key prefix applied to all cache keys, e.g. ",(0,c.jsx)(i.code,{children:'"MyApp:"'})]})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"ConfigurationOptions"})}),(0,c.jsxs)(i.td,{children:["Full ",(0,c.jsx)(i.code,{children:"StackExchange.Redis.ConfigurationOptions"})," for advanced scenarios (SSL, auth, retry policy)"]})]})]})]}),"\n",(0,c.jsx)(i.p,{children:"For production use with authentication and TLS:"}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:'cache.Configure(options =>\n{\n options.ConfigurationOptions = new StackExchange.Redis.ConfigurationOptions\n {\n EndPoints = { "my-redis.cache.windows.net:6380" },\n Password = "your-access-key",\n Ssl = true,\n AbortOnConnectFail = false\n };\n options.InstanceName = "MyApp:";\n});\n'})}),"\n",(0,c.jsx)(i.h2,{id:"expression-caching",children:"Expression caching"}),"\n",(0,c.jsxs)(i.p,{children:["Enable RCommon's internal expression caching optimization by calling ",(0,c.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions"}),":"]}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:'builder.Services.AddRCommon()\n .WithDistributedCaching(cache =>\n {\n cache.Configure(options =>\n {\n options.Configuration = "localhost:6379";\n });\n cache.CacheDynamicallyCompiledExpressions();\n });\n'})}),"\n",(0,c.jsx)(i.p,{children:"This registers:"}),"\n",(0,c.jsxs)(i.ul,{children:["\n",(0,c.jsxs)(i.li,{children:[(0,c.jsx)(i.code,{children:"ICacheService"})," as ",(0,c.jsx)(i.code,{children:"RedisCacheService"})]}),"\n",(0,c.jsxs)(i.li,{children:[(0,c.jsx)(i.code,{children:"CachingOptions"})," with ",(0,c.jsx)(i.code,{children:"CachingEnabled = true"})," and ",(0,c.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions = true"})]}),"\n",(0,c.jsxs)(i.li,{children:["A ",(0,c.jsx)(i.code,{children:"Func"})," factory for strategy-based resolution"]}),"\n"]}),"\n",(0,c.jsxs)(i.p,{children:["Note: the MassTransit documentation recommends using in-memory caching for expression caching because Redis introduces network overhead. Consider using ",(0,c.jsx)(i.code,{children:"RCommon.MemoryCache"})," for expression caching alongside Redis for application-level caching if performance is critical."]}),"\n",(0,c.jsx)(i.h2,{id:"using-icacheservice",children:"Using ICacheService"}),"\n",(0,c.jsxs)(i.p,{children:["Inject ",(0,c.jsx)(i.code,{children:"ICacheService"})," and use the get-or-create pattern. ",(0,c.jsx)(i.code,{children:"RedisCacheService"})," serializes values to JSON using ",(0,c.jsx)(i.code,{children:"IJsonSerializer"})," before storing them in Redis:"]}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:'public class ProductCatalogService\n{\n private readonly ICacheService _cache;\n private readonly IProductRepository _repository;\n\n public ProductCatalogService(ICacheService cache, IProductRepository repository)\n {\n _cache = cache;\n _repository = repository;\n }\n\n public async Task> GetActiveProductsAsync(CancellationToken ct)\n {\n var key = CacheKey.With(typeof(ProductCatalogService), "active-products");\n\n return await _cache.GetOrCreateAsync(key, () =>\n {\n // This runs only on a cache miss\n return _repository.FindAsync(p => p.IsActive).Result;\n });\n }\n}\n'})}),"\n",(0,c.jsx)(i.h2,{id:"how-rediscacheservice-works",children:"How RedisCacheService works"}),"\n",(0,c.jsxs)(i.p,{children:[(0,c.jsx)(i.code,{children:"RedisCacheService"})," implements the get-or-create pattern against ",(0,c.jsx)(i.code,{children:"IDistributedCache"}),":"]}),"\n",(0,c.jsxs)(i.ol,{children:["\n",(0,c.jsxs)(i.li,{children:["The key is converted to a string via ",(0,c.jsx)(i.code,{children:".ToString()"}),"."]}),"\n",(0,c.jsxs)(i.li,{children:[(0,c.jsx)(i.code,{children:"GetStringAsync(key)"})," is called. If the result is non-null, the JSON string is deserialized and returned."]}),"\n",(0,c.jsxs)(i.li,{children:["On a cache miss, the factory delegate is invoked, the result is serialized to JSON via ",(0,c.jsx)(i.code,{children:"IJsonSerializer"}),", stored with ",(0,c.jsx)(i.code,{children:"SetStringAsync"}),", and returned."]}),"\n"]}),"\n",(0,c.jsxs)(i.p,{children:["There is no built-in expiration set at the ",(0,c.jsx)(i.code,{children:"ICacheService"})," level; all entries are stored without a TTL by default. For expiration control, wrap ",(0,c.jsx)(i.code,{children:"IDistributedCache"})," directly and set ",(0,c.jsx)(i.code,{children:"DistributedCacheEntryOptions"}),"."]}),"\n",(0,c.jsx)(i.h2,{id:"json-serialization-dependency",children:"JSON serialization dependency"}),"\n",(0,c.jsxs)(i.p,{children:[(0,c.jsx)(i.code,{children:"RedisCacheService"})," depends on ",(0,c.jsx)(i.code,{children:"IJsonSerializer"})," from ",(0,c.jsx)(i.code,{children:"RCommon.Core"}),". Ensure a JSON serialization provider is registered before configuring Redis caching:"]}),"\n",(0,c.jsx)(i.pre,{children:(0,c.jsx)(i.code,{className:"language-csharp",children:'builder.Services.AddRCommon()\n .WithJsonSerialization() // or NewtonsoftJsonSerializer\n .WithDistributedCaching(cache =>\n {\n cache.Configure(options =>\n {\n options.Configuration = "localhost:6379";\n });\n });\n'})}),"\n",(0,c.jsx)(i.h2,{id:"api-summary",children:"API summary"}),"\n",(0,c.jsxs)(i.table,{children:[(0,c.jsx)(i.thead,{children:(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.th,{children:"Type"}),(0,c.jsx)(i.th,{children:"Package"}),(0,c.jsx)(i.th,{children:"Description"})]})}),(0,c.jsxs)(i.tbody,{children:[(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RedisCachingBuilder"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.RedisCache"})}),(0,c.jsx)(i.td,{children:"Concrete builder for Redis-backed distributed caching"})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"IRedisCachingBuilder"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.RedisCache"})}),(0,c.jsxs)(i.td,{children:["Marker interface extending ",(0,c.jsx)(i.code,{children:"IDistributedCachingBuilder"})]})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RedisCacheService"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.RedisCache"})}),(0,c.jsxs)(i.td,{children:[(0,c.jsx)(i.code,{children:"ICacheService"})," implementation backed by ",(0,c.jsx)(i.code,{children:"IDistributedCache"})," (Redis) with JSON serialization"]})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"Configure(options)"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.RedisCache"})}),(0,c.jsxs)(i.td,{children:["Extension on ",(0,c.jsx)(i.code,{children:"IRedisCachingBuilder"}),"; configures ",(0,c.jsx)(i.code,{children:"RedisCacheOptions"})," via ",(0,c.jsx)(i.code,{children:"AddStackExchangeRedisCache"})]})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"CacheDynamicallyCompiledExpressions()"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.RedisCache"})}),(0,c.jsxs)(i.td,{children:["Extension enabling expression caching optimization using ",(0,c.jsx)(i.code,{children:"RedisCacheService"})]})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"ICacheService"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.Caching"})}),(0,c.jsx)(i.td,{children:"Core abstraction for get-or-create caching"})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"CacheKey"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.Caching"})}),(0,c.jsx)(i.td,{children:"Strongly-typed cache key with factory methods"})]}),(0,c.jsxs)(i.tr,{children:[(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"WithDistributedCaching()"})}),(0,c.jsx)(i.td,{children:(0,c.jsx)(i.code,{children:"RCommon.Caching"})}),(0,c.jsxs)(i.td,{children:["Extension method on ",(0,c.jsx)(i.code,{children:"IRCommonBuilder"})]})]})]})]})]})}function x(e={}){const{wrapper:i}={...(0,s.R)(),...e.components};return i?(0,c.jsx)(i,{...e,children:(0,c.jsx)(h,{...e})}):h(e)}},75783(e,i,n){n.d(i,{A:()=>l});var c=n(30758);const s="container_xjrG",r="label_Y4p8",d="commandRow_FY5I",o="command_m7Qs",t="copyButton_u1GK";var a=n(86070);function l({packageName:e,version:i}){const[n,l]=(0,c.useState)(!1),h=i?`dotnet add package ${e} --version ${i}`:`dotnet add package ${e}`;return(0,a.jsxs)("div",{className:s,children:[(0,a.jsx)("div",{className:r,children:"NuGet Package"}),(0,a.jsxs)("div",{className:d,children:[(0,a.jsx)("code",{className:o,children:h}),(0,a.jsx)("button",{className:t,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:n?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,i,n){n.d(i,{R:()=>d,x:()=>o});var c=n(30758);const s={},r=c.createContext(s);function d(e){const i=c.useContext(r);return c.useMemo(function(){return"function"==typeof e?e(i):{...i,...e}},[i,e])}function o(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:d(e.components),c.createElement(r.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/6cfa6dfb.314d4cd8.js b/website/build/assets/js/6cfa6dfb.314d4cd8.js new file mode 100644 index 00000000..1cd6ba3c --- /dev/null +++ b/website/build/assets/js/6cfa6dfb.314d4cd8.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[6743],{25581(e,n,i){i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>o,default:()=>u,frontMatter:()=>c,metadata:()=>d,toc:()=>l});var r=i(86070),s=i(81753),t=i(75783);const c={title:"Authorization",sidebar_position:1,description:"RCommon.Security provides ICurrentUser and ICurrentPrincipalAccessor for claims-based identity resolution, plus Swashbuckle filters for OpenAPI authorization docs."},o="Authorization",d={id:"security-web/authorization",title:"Authorization",description:"RCommon.Security provides ICurrentUser and ICurrentPrincipalAccessor for claims-based identity resolution, plus Swashbuckle filters for OpenAPI authorization docs.",source:"@site/docs/security-web/authorization.mdx",sourceDirName:"security-web",slug:"/security-web/authorization",permalink:"/docs/next/security-web/authorization",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/security-web/authorization.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Authorization",sidebar_position:1,description:"RCommon.Security provides ICurrentUser and ICurrentPrincipalAccessor for claims-based identity resolution, plus Swashbuckle filters for OpenAPI authorization docs."},sidebar:"docsSidebar",previous:{title:"Security & Web",permalink:"/docs/next/category/security--web"},next:{title:"Web Utilities",permalink:"/docs/next/security-web/web-utilities"}},a={},l=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Claims and principal accessor (non-web / background services)",id:"claims-and-principal-accessor-non-web--background-services",level:3},{value:"Claims and principal accessor (ASP.NET Core web apps)",id:"claims-and-principal-accessor-aspnet-core-web-apps",level:3},{value:"Overriding claim type URIs",id:"overriding-claim-type-uris",level:3},{value:"Swashbuckle operation filters",id:"swashbuckle-operation-filters",level:3},{value:"Usage",id:"usage",level:2},{value:"Accessing the current user",id:"accessing-the-current-user",level:3},{value:"Reading roles",id:"reading-roles",level:3},{value:"Reading arbitrary claims",id:"reading-arbitrary-claims",level:3},{value:"Accessing the current client",id:"accessing-the-current-client",level:3},{value:"Temporarily replacing the principal",id:"temporarily-replacing-the-principal",level:3},{value:"Signaling authorization failures",id:"signaling-authorization-failures",level:3},{value:"Claims identity helpers",id:"claims-identity-helpers",level:3},{value:"API Summary",id:"api-summary",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"authorization",children:"Authorization"}),"\n",(0,r.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"RCommon.Security"})," provides the claims and principal accessor abstractions that RCommon uses internally for identity resolution. It does not replace ASP.NET Core's authorization pipeline \u2014 instead it sits alongside it, giving your application code a consistent way to read the current user's identity, roles, and claims without taking a hard dependency on ",(0,r.jsx)(n.code,{children:"HttpContext"})," or ",(0,r.jsx)(n.code,{children:"Thread.CurrentPrincipal"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"The core design is a layered stack:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"ICurrentPrincipalAccessor"})," \u2014 retrieves the current ",(0,r.jsx)(n.code,{children:"ClaimsPrincipal"})," and supports temporarily replacing it for a scoped context (useful in background workers and tests)."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"ICurrentUser"})," \u2014 reads well-known identity properties (ID, roles, tenant, arbitrary claims) from whatever principal the accessor provides."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"ICurrentClient"})," \u2014 reads the OAuth client ID claim from the same principal."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"ClaimTypesConst"})," \u2014 a static class of configurable claim type URIs so that you can remap standard claims to the values your identity provider actually issues."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"AuthorizationException"})," \u2014 a structured exception type for signaling authorization failures through the application tier."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"RCommon.Authorization.Web"})," adds two Swashbuckle operation filters that keep your OpenAPI/Swagger documentation accurate when your API uses authorization:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"AuthorizeCheckOperationFilter"})," \u2014 detects ",(0,r.jsx)(n.code,{children:"[Authorize]"})," on controllers and actions and adds 401/403 responses plus an OAuth2 security requirement to the generated operation."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"AuthorizationHeaderParameterOperationFilter"})," \u2014 detects ",(0,r.jsx)(n.code,{children:"AuthorizeFilter"})," in the MVC filter pipeline and adds a required ",(0,r.jsx)(n.code,{children:"Authorization"})," header parameter to the operation so that Swagger UI can accept a bearer token."]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(n.p,{children:"For the core security abstractions (principal accessor, current user, claims helpers):"}),"\n",(0,r.jsx)(t.A,{packageName:"RCommon.Security"}),"\n",(0,r.jsx)(n.p,{children:"For ASP.NET Core web apps that need HTTP-context-aware principal resolution:"}),"\n",(0,r.jsx)(t.A,{packageName:"RCommon.Web"}),"\n",(0,r.jsx)(n.p,{children:"For the Swashbuckle/OpenAPI authorization filters:"}),"\n",(0,r.jsx)(t.A,{packageName:"RCommon.Authorization.Web"}),"\n",(0,r.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsx)(n.h3,{id:"claims-and-principal-accessor-non-web--background-services",children:"Claims and principal accessor (non-web / background services)"}),"\n",(0,r.jsx)(n.p,{children:"Register the thread-based accessor when your application does not run inside ASP.NET Core request middleware:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\n\nbuilder.Services.AddRCommon(config =>\n{\n config.WithClaimsAndPrincipalAccessor();\n});\n"})}),"\n",(0,r.jsx)(n.p,{children:"This registers:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"ICurrentPrincipalAccessor"})," \u2192 ",(0,r.jsx)(n.code,{children:"ThreadCurrentPrincipalAccessor"})," (reads ",(0,r.jsx)(n.code,{children:"Thread.CurrentPrincipal"}),")"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"ICurrentUser"})," \u2192 ",(0,r.jsx)(n.code,{children:"CurrentUser"})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"ICurrentClient"})," \u2192 ",(0,r.jsx)(n.code,{children:"CurrentClient"})]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"ITenantIdAccessor"})," \u2192 ",(0,r.jsx)(n.code,{children:"ClaimsTenantIdAccessor"})]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"claims-and-principal-accessor-aspnet-core-web-apps",children:"Claims and principal accessor (ASP.NET Core web apps)"}),"\n",(0,r.jsxs)(n.p,{children:["Use the web variant which reads the principal from ",(0,r.jsx)(n.code,{children:"HttpContext.User"})," instead:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\n\nbuilder.Services.AddRCommon(config =>\n{\n config.WithClaimsAndPrincipalAccessorForWeb();\n});\n"})}),"\n",(0,r.jsxs)(n.p,{children:["This registers the same interfaces but substitutes ",(0,r.jsx)(n.code,{children:"HttpContextCurrentPrincipalAccessor"})," for ",(0,r.jsx)(n.code,{children:"ThreadCurrentPrincipalAccessor"}),", and calls ",(0,r.jsx)(n.code,{children:"AddHttpContextAccessor()"})," automatically."]}),"\n",(0,r.jsx)(n.admonition,{title:"Modular composition",type:"tip",children:(0,r.jsxs)(n.p,{children:["Both ",(0,r.jsx)(n.code,{children:"WithClaimsAndPrincipalAccessor()"})," and ",(0,r.jsx)(n.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," are ",(0,r.jsx)(n.code,{children:"TryAdd"}),"-hardened \u2014 repeat calls from multiple modules are idempotent. The principal accessor, ",(0,r.jsx)(n.code,{children:"ICurrentUser"}),", ",(0,r.jsx)(n.code,{children:"ICurrentClient"}),", and ",(0,r.jsx)(n.code,{children:"ITenantIdAccessor"})," registrations are each added exactly once. Do not mix the two variants in the same process: ",(0,r.jsx)(n.code,{children:"TryAdd"})," keeps whichever ran first and silently ignores the other, which can leave a web host running with ",(0,r.jsx)(n.code,{children:"ThreadCurrentPrincipalAccessor"})," (or vice versa). Pick one variant in the host's composition root. See ",(0,r.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,r.jsx)(n.h3,{id:"overriding-claim-type-uris",children:"Overriding claim type URIs"}),"\n",(0,r.jsxs)(n.p,{children:["If your identity provider uses non-standard claim type URIs, override the static properties on ",(0,r.jsx)(n.code,{children:"ClaimTypesConst"})," once at startup before any requests are processed:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon.Security.Claims;\n\n// Map to the short claim names issued by your OIDC provider.\n// Configure must be called once at startup, before any property is accessed.\nClaimTypesConst.Configure(options =>\n{\n options.UserId = "sub";\n options.Role = "roles";\n options.Email = "email";\n options.TenantId = "tenant_id";\n options.ClientId = "client_id";\n});\n'})}),"\n",(0,r.jsx)(n.h3,{id:"swashbuckle-operation-filters",children:"Swashbuckle operation filters"}),"\n",(0,r.jsx)(n.p,{children:"Add both filters to your Swagger generation options:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon.Authorization.Web.Filters;\n\nbuilder.Services.AddSwaggerGen(options =>\n{\n options.OperationFilter();\n options.OperationFilter();\n\n // Register your OAuth2 security scheme so that the filters can reference it.\n options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme\n {\n Type = SecuritySchemeType.OAuth2,\n Flows = new OpenApiOAuthFlows\n {\n AuthorizationCode = new OpenApiOAuthFlow\n {\n AuthorizationUrl = new Uri("https://your-idp/connect/authorize"),\n TokenUrl = new Uri("https://your-idp/connect/token"),\n Scopes = new Dictionary { ["api"] = "API access" }\n }\n }\n });\n});\n'})}),"\n",(0,r.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,r.jsx)(n.h3,{id:"accessing-the-current-user",children:"Accessing the current user"}),"\n",(0,r.jsxs)(n.p,{children:["Inject ",(0,r.jsx)(n.code,{children:"ICurrentUser"})," wherever you need identity information:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon.Security.Users;\n\npublic class OrderCommandHandler\n{\n private readonly ICurrentUser _currentUser;\n private readonly IGraphRepository _orders;\n\n public OrderCommandHandler(ICurrentUser currentUser, IGraphRepository orders)\n {\n _currentUser = currentUser;\n _orders = orders;\n }\n\n public async Task Handle(PlaceOrderCommand command, CancellationToken ct)\n {\n if (!_currentUser.IsAuthenticated)\n throw new AuthorizationException("You must be signed in to place an order.");\n\n var order = new Order\n {\n CustomerId = _currentUser.Id!,\n CustomerName = _currentUser.FindClaimValue(ClaimTypes.GivenName) ?? "Unknown",\n Total = command.Total\n };\n\n await _orders.AddAsync(order, ct);\n }\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"reading-roles",children:"Reading roles"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'string[] roles = _currentUser.Roles; // distinct role values from ClaimTypesConst.Role claims\n\nif (!_currentUser.Roles.Contains("Administrator"))\n throw new AuthorizationException("Only administrators can perform this action.", "INSUFFICIENT_ROLE");\n'})}),"\n",(0,r.jsx)(n.h3,{id:"reading-arbitrary-claims",children:"Reading arbitrary claims"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'// Find the first matching claim.\nClaim? claim = _currentUser.FindClaim("custom:department");\n\n// Find all matching claims.\nClaim[] allDeptClaims = _currentUser.FindClaims("custom:department");\n\n// Find a claim value as a string.\nstring? department = _currentUser.FindClaimValue("custom:department");\n'})}),"\n",(0,r.jsx)(n.h3,{id:"accessing-the-current-client",children:"Accessing the current client"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon.Security.Clients;\n\npublic class ApiAuditMiddleware\n{\n private readonly ICurrentClient _currentClient;\n\n public ApiAuditMiddleware(ICurrentClient currentClient)\n {\n _currentClient = currentClient;\n }\n\n public void LogRequest(string path)\n {\n if (_currentClient.IsAuthenticated)\n {\n Console.WriteLine($"Client {_currentClient.Id} called {path}");\n }\n }\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"temporarily-replacing-the-principal",children:"Temporarily replacing the principal"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"ICurrentPrincipalAccessor.Change"})," replaces the current principal for the lifetime of the returned ",(0,r.jsx)(n.code,{children:"IDisposable"}),". This is useful in background jobs and integration tests:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon.Security.Claims;\nusing System.Security.Claims;\n\npublic async Task RunAsServiceAccountAsync(\n ICurrentPrincipalAccessor principalAccessor,\n Func work)\n{\n var identity = new ClaimsIdentity(new[]\n {\n new Claim(ClaimTypesConst.UserId, Guid.NewGuid().ToString()),\n new Claim(ClaimTypesConst.Role, "ServiceAccount")\n }, "ServiceAccount");\n\n using (principalAccessor.Change(new ClaimsPrincipal(identity)))\n {\n await work();\n // Previous principal is restored when the using block exits.\n }\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["The extension overloads let you pass a single ",(0,r.jsx)(n.code,{children:"Claim"}),", a collection of claims, or a ",(0,r.jsx)(n.code,{children:"ClaimsIdentity"})," directly:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using (principalAccessor.Change(new Claim(ClaimTypesConst.Role, "Tester")))\n{\n // ...\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"signaling-authorization-failures",children:"Signaling authorization failures"}),"\n",(0,r.jsxs)(n.p,{children:["Throw ",(0,r.jsx)(n.code,{children:"AuthorizationException"})," from application-layer code when a rule is violated. Global exception handlers or middleware can then translate it to an appropriate HTTP 403 response:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon.Security.Authorization;\n\npublic void EnsureCanEdit(Document document)\n{\n if (document.OwnerId != _currentUser.Id)\n {\n throw new AuthorizationException(\n message: "You do not have permission to edit this document.",\n code: "DOCUMENT_ACCESS_DENIED")\n .WithData("DocumentId", document.Id)\n .WithData("UserId", _currentUser.Id);\n }\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"claims-identity-helpers",children:"Claims identity helpers"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"ClaimsIdentityExtensions"})," provides fluent extension methods for managing claims:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon.Security;\n\n// Add a claim only if one with the same type does not already exist.\nidentity.AddIfNotContains(new Claim("custom:role", "Editor"));\n\n// Remove all claims of the same type and set a new value.\nidentity.AddOrReplace(new Claim("custom:role", "Administrator"));\n\n// Add an identity to a principal only if the same authentication type is not already present.\nprincipal.AddIdentityIfNotContains(new ClaimsIdentity(claims, "Cookie"));\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Extract well-known values directly from a ",(0,r.jsx)(n.code,{children:"ClaimsPrincipal"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"string? userId = principal.FindUserId(); // reads ClaimTypesConst.UserId\nstring? tenant = principal.FindTenantId(); // reads ClaimTypesConst.TenantId\nstring? client = principal.FindClientId(); // reads ClaimTypesConst.ClientId\n"})}),"\n",(0,r.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Package"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ICurrentPrincipalAccessor"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.Security"})}),(0,r.jsxs)(n.td,{children:["Provides the current ",(0,r.jsx)(n.code,{children:"ClaimsPrincipal"}),"; supports scoped override via ",(0,r.jsx)(n.code,{children:"Change()"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"CurrentPrincipalAccessorBase"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.Security"})}),(0,r.jsxs)(n.td,{children:["Abstract base implementing ",(0,r.jsx)(n.code,{children:"Change()"})," with ",(0,r.jsx)(n.code,{children:"AsyncLocal"})," storage"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ThreadCurrentPrincipalAccessor"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.Security"})}),(0,r.jsxs)(n.td,{children:["Default implementation; reads ",(0,r.jsx)(n.code,{children:"Thread.CurrentPrincipal"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"CurrentPrincipalAccessorExtensions"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.Security"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"Change(Claim)"}),", ",(0,r.jsx)(n.code,{children:"Change(IEnumerable)"}),", ",(0,r.jsx)(n.code,{children:"Change(ClaimsIdentity)"})," overloads"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ICurrentUser"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.Security"})}),(0,r.jsxs)(n.td,{children:["Exposes ",(0,r.jsx)(n.code,{children:"Id"}),", ",(0,r.jsx)(n.code,{children:"IsAuthenticated"}),", ",(0,r.jsx)(n.code,{children:"Roles"}),", ",(0,r.jsx)(n.code,{children:"TenantId"}),", ",(0,r.jsx)(n.code,{children:"FindClaim"}),", ",(0,r.jsx)(n.code,{children:"FindClaims"}),", ",(0,r.jsx)(n.code,{children:"GetAllClaims"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"CurrentUser"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.Security"})}),(0,r.jsxs)(n.td,{children:["Default ",(0,r.jsx)(n.code,{children:"ICurrentUser"})," implementation backed by ",(0,r.jsx)(n.code,{children:"ICurrentPrincipalAccessor"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"CurrentUserExtensions"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.Security"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"FindClaimValue(string)"}),", ",(0,r.jsx)(n.code,{children:"GetId()"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ICurrentClient"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.Security"})}),(0,r.jsxs)(n.td,{children:["Exposes ",(0,r.jsx)(n.code,{children:"Id"})," and ",(0,r.jsx)(n.code,{children:"IsAuthenticated"})," for OAuth client identities"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"CurrentClient"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.Security"})}),(0,r.jsxs)(n.td,{children:["Default ",(0,r.jsx)(n.code,{children:"ICurrentClient"})," backed by ",(0,r.jsx)(n.code,{children:"ICurrentPrincipalAccessor"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ClaimTypesConst"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.Security"})}),(0,r.jsxs)(n.td,{children:["Configure-once claim type URIs via ",(0,r.jsx)(n.code,{children:"Configure(Action)"}),": ",(0,r.jsx)(n.code,{children:"UserName"}),", ",(0,r.jsx)(n.code,{children:"Name"}),", ",(0,r.jsx)(n.code,{children:"SurName"}),", ",(0,r.jsx)(n.code,{children:"UserId"}),", ",(0,r.jsx)(n.code,{children:"Role"}),", ",(0,r.jsx)(n.code,{children:"Email"}),", ",(0,r.jsx)(n.code,{children:"TenantId"}),", ",(0,r.jsx)(n.code,{children:"ClientId"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ClaimsIdentityExtensions"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.Security"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"FindUserId"}),", ",(0,r.jsx)(n.code,{children:"FindTenantId"}),", ",(0,r.jsx)(n.code,{children:"FindClientId"}),", ",(0,r.jsx)(n.code,{children:"AddIfNotContains"}),", ",(0,r.jsx)(n.code,{children:"AddOrReplace"}),", ",(0,r.jsx)(n.code,{children:"AddIdentityIfNotContains"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"AuthorizationException"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.Security"})}),(0,r.jsxs)(n.td,{children:["Application-layer exception for access-denied scenarios; carries ",(0,r.jsx)(n.code,{children:"Code"}),", ",(0,r.jsx)(n.code,{children:"LogLevel"}),", and fluent ",(0,r.jsx)(n.code,{children:"WithData()"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"AuthorizeCheckOperationFilter"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.Authorization.Web"})}),(0,r.jsxs)(n.td,{children:["Swashbuckle filter: adds 401/403 responses and OAuth2 security requirement to ",(0,r.jsx)(n.code,{children:"[Authorize]"}),"-decorated operations"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"AuthorizationHeaderParameterOperationFilter"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.Authorization.Web"})}),(0,r.jsxs)(n.td,{children:["Swashbuckle filter: adds a required ",(0,r.jsx)(n.code,{children:"Authorization"})," header parameter to operations protected by ",(0,r.jsx)(n.code,{children:"AuthorizeFilter"})]})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},75783(e,n,i){i.d(n,{A:()=>l});var r=i(30758);const s="container_xjrG",t="label_Y4p8",c="commandRow_FY5I",o="command_m7Qs",d="copyButton_u1GK";var a=i(86070);function l({packageName:e,version:n}){const[i,l]=(0,r.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,a.jsxs)("div",{className:s,children:[(0,a.jsx)("div",{className:t,children:"NuGet Package"}),(0,a.jsxs)("div",{className:c,children:[(0,a.jsx)("code",{className:o,children:h}),(0,a.jsx)("button",{className:d,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:i?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,i){i.d(n,{R:()=>c,x:()=>o});var r=i(30758);const s={},t=r.createContext(s);function c(e){const n=r.useContext(t);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:c(e.components),r.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/6cfa6dfb.8fd6ae54.js b/website/build/assets/js/6cfa6dfb.8fd6ae54.js deleted file mode 100644 index 26682f79..00000000 --- a/website/build/assets/js/6cfa6dfb.8fd6ae54.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[6743],{25581(e,r,n){n.r(r),n.d(r,{assets:()=>o,contentTitle:()=>d,default:()=>u,frontMatter:()=>c,metadata:()=>a,toc:()=>l});var i=n(86070),s=n(81753),t=n(75783);const c={title:"Authorization",sidebar_position:1,description:"RCommon.Security provides ICurrentUser and ICurrentPrincipalAccessor for claims-based identity resolution, plus Swashbuckle filters for OpenAPI authorization docs."},d="Authorization",a={id:"security-web/authorization",title:"Authorization",description:"RCommon.Security provides ICurrentUser and ICurrentPrincipalAccessor for claims-based identity resolution, plus Swashbuckle filters for OpenAPI authorization docs.",source:"@site/docs/security-web/authorization.mdx",sourceDirName:"security-web",slug:"/security-web/authorization",permalink:"/docs/next/security-web/authorization",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/security-web/authorization.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Authorization",sidebar_position:1,description:"RCommon.Security provides ICurrentUser and ICurrentPrincipalAccessor for claims-based identity resolution, plus Swashbuckle filters for OpenAPI authorization docs."},sidebar:"docsSidebar",previous:{title:"Security & Web",permalink:"/docs/next/category/security--web"},next:{title:"Web Utilities",permalink:"/docs/next/security-web/web-utilities"}},o={},l=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Claims and principal accessor (non-web / background services)",id:"claims-and-principal-accessor-non-web--background-services",level:3},{value:"Claims and principal accessor (ASP.NET Core web apps)",id:"claims-and-principal-accessor-aspnet-core-web-apps",level:3},{value:"Overriding claim type URIs",id:"overriding-claim-type-uris",level:3},{value:"Swashbuckle operation filters",id:"swashbuckle-operation-filters",level:3},{value:"Usage",id:"usage",level:2},{value:"Accessing the current user",id:"accessing-the-current-user",level:3},{value:"Reading roles",id:"reading-roles",level:3},{value:"Reading arbitrary claims",id:"reading-arbitrary-claims",level:3},{value:"Accessing the current client",id:"accessing-the-current-client",level:3},{value:"Temporarily replacing the principal",id:"temporarily-replacing-the-principal",level:3},{value:"Signaling authorization failures",id:"signaling-authorization-failures",level:3},{value:"Claims identity helpers",id:"claims-identity-helpers",level:3},{value:"API Summary",id:"api-summary",level:2}];function h(e){const r={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(r.h1,{id:"authorization",children:"Authorization"}),"\n",(0,i.jsx)(r.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"RCommon.Security"})," provides the claims and principal accessor abstractions that RCommon uses internally for identity resolution. It does not replace ASP.NET Core's authorization pipeline \u2014 instead it sits alongside it, giving your application code a consistent way to read the current user's identity, roles, and claims without taking a hard dependency on ",(0,i.jsx)(r.code,{children:"HttpContext"})," or ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"}),"."]}),"\n",(0,i.jsx)(r.p,{children:"The core design is a layered stack:"}),"\n",(0,i.jsxs)(r.ul,{children:["\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})," \u2014 retrieves the current ",(0,i.jsx)(r.code,{children:"ClaimsPrincipal"})," and supports temporarily replacing it for a scoped context (useful in background workers and tests)."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentUser"})," \u2014 reads well-known identity properties (ID, roles, tenant, arbitrary claims) from whatever principal the accessor provides."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentClient"})," \u2014 reads the OAuth client ID claim from the same principal."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ClaimTypesConst"})," \u2014 a static class of configurable claim type URIs so that you can remap standard claims to the values your identity provider actually issues."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"AuthorizationException"})," \u2014 a structured exception type for signaling authorization failures through the application tier."]}),"\n"]}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"RCommon.Authorization.Web"})," adds two Swashbuckle operation filters that keep your OpenAPI/Swagger documentation accurate when your API uses authorization:"]}),"\n",(0,i.jsxs)(r.ul,{children:["\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"AuthorizeCheckOperationFilter"})," \u2014 detects ",(0,i.jsx)(r.code,{children:"[Authorize]"})," on controllers and actions and adds 401/403 responses plus an OAuth2 security requirement to the generated operation."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"AuthorizationHeaderParameterOperationFilter"})," \u2014 detects ",(0,i.jsx)(r.code,{children:"AuthorizeFilter"})," in the MVC filter pipeline and adds a required ",(0,i.jsx)(r.code,{children:"Authorization"})," header parameter to the operation so that Swagger UI can accept a bearer token."]}),"\n"]}),"\n",(0,i.jsx)(r.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(r.p,{children:"For the core security abstractions (principal accessor, current user, claims helpers):"}),"\n",(0,i.jsx)(t.A,{packageName:"RCommon.Security"}),"\n",(0,i.jsx)(r.p,{children:"For ASP.NET Core web apps that need HTTP-context-aware principal resolution:"}),"\n",(0,i.jsx)(t.A,{packageName:"RCommon.Web"}),"\n",(0,i.jsx)(r.p,{children:"For the Swashbuckle/OpenAPI authorization filters:"}),"\n",(0,i.jsx)(t.A,{packageName:"RCommon.Authorization.Web"}),"\n",(0,i.jsx)(r.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsx)(r.h3,{id:"claims-and-principal-accessor-non-web--background-services",children:"Claims and principal accessor (non-web / background services)"}),"\n",(0,i.jsx)(r.p,{children:"Register the thread-based accessor when your application does not run inside ASP.NET Core request middleware:"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"using RCommon;\r\n\r\nbuilder.Services.AddRCommon(config =>\r\n{\r\n config.WithClaimsAndPrincipalAccessor();\r\n});\n"})}),"\n",(0,i.jsx)(r.p,{children:"This registers:"}),"\n",(0,i.jsxs)(r.ul,{children:["\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})," \u2192 ",(0,i.jsx)(r.code,{children:"ThreadCurrentPrincipalAccessor"})," (reads ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"}),")"]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentUser"})," \u2192 ",(0,i.jsx)(r.code,{children:"CurrentUser"})]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentClient"})," \u2192 ",(0,i.jsx)(r.code,{children:"CurrentClient"})]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ITenantIdAccessor"})," \u2192 ",(0,i.jsx)(r.code,{children:"ClaimsTenantIdAccessor"})]}),"\n"]}),"\n",(0,i.jsx)(r.h3,{id:"claims-and-principal-accessor-aspnet-core-web-apps",children:"Claims and principal accessor (ASP.NET Core web apps)"}),"\n",(0,i.jsxs)(r.p,{children:["Use the web variant which reads the principal from ",(0,i.jsx)(r.code,{children:"HttpContext.User"})," instead:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"using RCommon;\r\n\r\nbuilder.Services.AddRCommon(config =>\r\n{\r\n config.WithClaimsAndPrincipalAccessorForWeb();\r\n});\n"})}),"\n",(0,i.jsxs)(r.p,{children:["This registers the same interfaces but substitutes ",(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"})," for ",(0,i.jsx)(r.code,{children:"ThreadCurrentPrincipalAccessor"}),", and calls ",(0,i.jsx)(r.code,{children:"AddHttpContextAccessor()"})," automatically."]}),"\n",(0,i.jsx)(r.h3,{id:"overriding-claim-type-uris",children:"Overriding claim type URIs"}),"\n",(0,i.jsxs)(r.p,{children:["If your identity provider uses non-standard claim type URIs, override the static properties on ",(0,i.jsx)(r.code,{children:"ClaimTypesConst"})," once at startup before any requests are processed:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Claims;\r\n\r\n// Map to the short claim names issued by your OIDC provider.\r\nClaimTypesConst.UserId = "sub";\r\nClaimTypesConst.Role = "roles";\r\nClaimTypesConst.Email = "email";\r\nClaimTypesConst.TenantId = "tenant_id";\r\nClaimTypesConst.ClientId = "client_id";\n'})}),"\n",(0,i.jsx)(r.h3,{id:"swashbuckle-operation-filters",children:"Swashbuckle operation filters"}),"\n",(0,i.jsx)(r.p,{children:"Add both filters to your Swagger generation options:"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Authorization.Web.Filters;\r\n\r\nbuilder.Services.AddSwaggerGen(options =>\r\n{\r\n options.OperationFilter();\r\n options.OperationFilter();\r\n\r\n // Register your OAuth2 security scheme so that the filters can reference it.\r\n options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme\r\n {\r\n Type = SecuritySchemeType.OAuth2,\r\n Flows = new OpenApiOAuthFlows\r\n {\r\n AuthorizationCode = new OpenApiOAuthFlow\r\n {\r\n AuthorizationUrl = new Uri("https://your-idp/connect/authorize"),\r\n TokenUrl = new Uri("https://your-idp/connect/token"),\r\n Scopes = new Dictionary { ["api"] = "API access" }\r\n }\r\n }\r\n });\r\n});\n'})}),"\n",(0,i.jsx)(r.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(r.h3,{id:"accessing-the-current-user",children:"Accessing the current user"}),"\n",(0,i.jsxs)(r.p,{children:["Inject ",(0,i.jsx)(r.code,{children:"ICurrentUser"})," wherever you need identity information:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Users;\r\n\r\npublic class OrderCommandHandler\r\n{\r\n private readonly ICurrentUser _currentUser;\r\n private readonly IGraphRepository _orders;\r\n\r\n public OrderCommandHandler(ICurrentUser currentUser, IGraphRepository orders)\r\n {\r\n _currentUser = currentUser;\r\n _orders = orders;\r\n }\r\n\r\n public async Task Handle(PlaceOrderCommand command, CancellationToken ct)\r\n {\r\n if (!_currentUser.IsAuthenticated)\r\n throw new AuthorizationException("You must be signed in to place an order.");\r\n\r\n var order = new Order\r\n {\r\n CustomerId = _currentUser.Id!.Value,\r\n CustomerName = _currentUser.FindClaimValue(ClaimTypes.GivenName) ?? "Unknown",\r\n Total = command.Total\r\n };\r\n\r\n await _orders.AddAsync(order, ct);\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"reading-roles",children:"Reading roles"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'string[] roles = _currentUser.Roles; // distinct role values from ClaimTypesConst.Role claims\r\n\r\nif (!_currentUser.Roles.Contains("Administrator"))\r\n throw new AuthorizationException("Only administrators can perform this action.", "INSUFFICIENT_ROLE");\n'})}),"\n",(0,i.jsx)(r.h3,{id:"reading-arbitrary-claims",children:"Reading arbitrary claims"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'// Find the first matching claim.\r\nClaim? claim = _currentUser.FindClaim("custom:department");\r\n\r\n// Find all matching claims.\r\nClaim[] allDeptClaims = _currentUser.FindClaims("custom:department");\r\n\r\n// Find a claim value as a string.\r\nstring? department = _currentUser.FindClaimValue("custom:department");\r\n\r\n// Find a claim value converted to a specific struct type.\r\nint employeeNumber = _currentUser.FindClaimValue("custom:employee_number");\n'})}),"\n",(0,i.jsx)(r.h3,{id:"accessing-the-current-client",children:"Accessing the current client"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Clients;\r\n\r\npublic class ApiAuditMiddleware\r\n{\r\n private readonly ICurrentClient _currentClient;\r\n\r\n public ApiAuditMiddleware(ICurrentClient currentClient)\r\n {\r\n _currentClient = currentClient;\r\n }\r\n\r\n public void LogRequest(string path)\r\n {\r\n if (_currentClient.IsAuthenticated)\r\n {\r\n Console.WriteLine($"Client {_currentClient.Id} called {path}");\r\n }\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"temporarily-replacing-the-principal",children:"Temporarily replacing the principal"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor.Change"})," replaces the current principal for the lifetime of the returned ",(0,i.jsx)(r.code,{children:"IDisposable"}),". This is useful in background jobs and integration tests:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Claims;\r\nusing System.Security.Claims;\r\n\r\npublic async Task RunAsServiceAccountAsync(\r\n ICurrentPrincipalAccessor principalAccessor,\r\n Func work)\r\n{\r\n var identity = new ClaimsIdentity(new[]\r\n {\r\n new Claim(ClaimTypesConst.UserId, Guid.NewGuid().ToString()),\r\n new Claim(ClaimTypesConst.Role, "ServiceAccount")\r\n }, "ServiceAccount");\r\n\r\n using (principalAccessor.Change(new ClaimsPrincipal(identity)))\r\n {\r\n await work();\r\n // Previous principal is restored when the using block exits.\r\n }\r\n}\n'})}),"\n",(0,i.jsxs)(r.p,{children:["The extension overloads let you pass a single ",(0,i.jsx)(r.code,{children:"Claim"}),", a collection of claims, or a ",(0,i.jsx)(r.code,{children:"ClaimsIdentity"})," directly:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using (principalAccessor.Change(new Claim(ClaimTypesConst.Role, "Tester")))\r\n{\r\n // ...\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"signaling-authorization-failures",children:"Signaling authorization failures"}),"\n",(0,i.jsxs)(r.p,{children:["Throw ",(0,i.jsx)(r.code,{children:"AuthorizationException"})," from application-layer code when a rule is violated. Global exception handlers or middleware can then translate it to an appropriate HTTP 403 response:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Authorization;\r\n\r\npublic void EnsureCanEdit(Document document)\r\n{\r\n if (document.OwnerId != _currentUser.Id)\r\n {\r\n throw new AuthorizationException(\r\n message: "You do not have permission to edit this document.",\r\n code: "DOCUMENT_ACCESS_DENIED")\r\n .WithData("DocumentId", document.Id)\r\n .WithData("UserId", _currentUser.Id);\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"claims-identity-helpers",children:"Claims identity helpers"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"ClaimsIdentityExtensions"})," provides fluent extension methods for managing claims:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security;\r\n\r\n// Add a claim only if one with the same type does not already exist.\r\nidentity.AddIfNotContains(new Claim("custom:role", "Editor"));\r\n\r\n// Remove all claims of the same type and set a new value.\r\nidentity.AddOrReplace(new Claim("custom:role", "Administrator"));\r\n\r\n// Add an identity to a principal only if the same authentication type is not already present.\r\nprincipal.AddIdentityIfNotContains(new ClaimsIdentity(claims, "Cookie"));\n'})}),"\n",(0,i.jsxs)(r.p,{children:["Extract well-known values directly from a ",(0,i.jsx)(r.code,{children:"ClaimsPrincipal"}),":"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"Guid? userId = principal.FindUserId(); // parses ClaimTypesConst.UserId as Guid\r\nstring? tenant = principal.FindTenantId(); // reads ClaimTypesConst.TenantId\r\nstring? client = principal.FindClientId(); // reads ClaimTypesConst.ClientId\n"})}),"\n",(0,i.jsx)(r.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(r.table,{children:[(0,i.jsx)(r.thead,{children:(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.th,{children:"Type"}),(0,i.jsx)(r.th,{children:"Package"}),(0,i.jsx)(r.th,{children:"Description"})]})}),(0,i.jsxs)(r.tbody,{children:[(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Provides the current ",(0,i.jsx)(r.code,{children:"ClaimsPrincipal"}),"; supports scoped override via ",(0,i.jsx)(r.code,{children:"Change()"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentPrincipalAccessorBase"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Abstract base implementing ",(0,i.jsx)(r.code,{children:"Change()"})," with ",(0,i.jsx)(r.code,{children:"AsyncLocal"})," storage"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ThreadCurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Default implementation; reads ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentPrincipalAccessorExtensions"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:[(0,i.jsx)(r.code,{children:"Change(Claim)"}),", ",(0,i.jsx)(r.code,{children:"Change(IEnumerable)"}),", ",(0,i.jsx)(r.code,{children:"Change(ClaimsIdentity)"})," overloads"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentUser"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Exposes ",(0,i.jsx)(r.code,{children:"Id"}),", ",(0,i.jsx)(r.code,{children:"IsAuthenticated"}),", ",(0,i.jsx)(r.code,{children:"Roles"}),", ",(0,i.jsx)(r.code,{children:"TenantId"}),", ",(0,i.jsx)(r.code,{children:"FindClaim"}),", ",(0,i.jsx)(r.code,{children:"FindClaims"}),", ",(0,i.jsx)(r.code,{children:"GetAllClaims"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentUser"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Default ",(0,i.jsx)(r.code,{children:"ICurrentUser"})," implementation backed by ",(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentUserExtensions"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:[(0,i.jsx)(r.code,{children:"FindClaimValue(string)"}),", ",(0,i.jsx)(r.code,{children:"FindClaimValue(string)"}),", ",(0,i.jsx)(r.code,{children:"GetId()"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentClient"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Exposes ",(0,i.jsx)(r.code,{children:"Id"})," and ",(0,i.jsx)(r.code,{children:"IsAuthenticated"})," for OAuth client identities"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentClient"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Default ",(0,i.jsx)(r.code,{children:"ICurrentClient"})," backed by ",(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ClaimTypesConst"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Configurable claim type URIs: ",(0,i.jsx)(r.code,{children:"UserName"}),", ",(0,i.jsx)(r.code,{children:"Name"}),", ",(0,i.jsx)(r.code,{children:"SurName"}),", ",(0,i.jsx)(r.code,{children:"UserId"}),", ",(0,i.jsx)(r.code,{children:"Role"}),", ",(0,i.jsx)(r.code,{children:"Email"}),", ",(0,i.jsx)(r.code,{children:"TenantId"}),", ",(0,i.jsx)(r.code,{children:"ClientId"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ClaimsIdentityExtensions"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:[(0,i.jsx)(r.code,{children:"FindUserId"}),", ",(0,i.jsx)(r.code,{children:"FindTenantId"}),", ",(0,i.jsx)(r.code,{children:"FindClientId"}),", ",(0,i.jsx)(r.code,{children:"AddIfNotContains"}),", ",(0,i.jsx)(r.code,{children:"AddOrReplace"}),", ",(0,i.jsx)(r.code,{children:"AddIdentityIfNotContains"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"AuthorizationException"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Application-layer exception for access-denied scenarios; carries ",(0,i.jsx)(r.code,{children:"Code"}),", ",(0,i.jsx)(r.code,{children:"LogLevel"}),", and fluent ",(0,i.jsx)(r.code,{children:"WithData()"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"AuthorizeCheckOperationFilter"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Authorization.Web"})}),(0,i.jsxs)(r.td,{children:["Swashbuckle filter: adds 401/403 responses and OAuth2 security requirement to ",(0,i.jsx)(r.code,{children:"[Authorize]"}),"-decorated operations"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"AuthorizationHeaderParameterOperationFilter"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Authorization.Web"})}),(0,i.jsxs)(r.td,{children:["Swashbuckle filter: adds a required ",(0,i.jsx)(r.code,{children:"Authorization"})," header parameter to operations protected by ",(0,i.jsx)(r.code,{children:"AuthorizeFilter"})]})]})]})]})]})}function u(e={}){const{wrapper:r}={...(0,s.R)(),...e.components};return r?(0,i.jsx)(r,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},75783(e,r,n){n.d(r,{A:()=>l});var i=n(30758);const s="container_xjrG",t="label_Y4p8",c="commandRow_FY5I",d="command_m7Qs",a="copyButton_u1GK";var o=n(86070);function l({packageName:e,version:r}){const[n,l]=(0,i.useState)(!1),h=r?`dotnet add package ${e} --version ${r}`:`dotnet add package ${e}`;return(0,o.jsxs)("div",{className:s,children:[(0,o.jsx)("div",{className:t,children:"NuGet Package"}),(0,o.jsxs)("div",{className:c,children:[(0,o.jsx)("code",{className:d,children:h}),(0,o.jsx)("button",{className:a,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:n?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,r,n){n.d(r,{R:()=>c,x:()=>d});var i=n(30758);const s={},t=i.createContext(s);function c(e){const r=i.useContext(t);return i.useMemo(function(){return"function"==typeof e?e(r):{...r,...e}},[r,e])}function d(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:c(e.components),i.createElement(t.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/6d12448e.4f619896.js b/website/build/assets/js/6d12448e.4f619896.js deleted file mode 100644 index c3246749..00000000 --- a/website/build/assets/js/6d12448e.4f619896.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[5949],{78422(e,n,t){t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>c,default:()=>u,frontMatter:()=>s,metadata:()=>a,toc:()=>l});var i=t(86070),o=t(81753),r=t(75783);const s={title:"Unit of Work",sidebar_position:3,description:"Coordinate writes across multiple repositories in a single TransactionScope using RCommon's IUnitOfWorkFactory, with MediatR and Wolverine pipeline integration support."},c="Unit of Work",a={id:"persistence/unit-of-work",title:"Unit of Work",description:"Coordinate writes across multiple repositories in a single TransactionScope using RCommon's IUnitOfWorkFactory, with MediatR and Wolverine pipeline integration support.",source:"@site/docs/persistence/unit-of-work.mdx",sourceDirName:"persistence",slug:"/persistence/unit-of-work",permalink:"/docs/next/persistence/unit-of-work",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/persistence/unit-of-work.mdx",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"Unit of Work",sidebar_position:3,description:"Coordinate writes across multiple repositories in a single TransactionScope using RCommon's IUnitOfWorkFactory, with MediatR and Wolverine pipeline integration support."},sidebar:"docsSidebar",previous:{title:"Specifications",permalink:"/docs/next/persistence/specifications"},next:{title:"Entity Framework Core",permalink:"/docs/next/persistence/efcore"}},d={},l=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Usage",id:"usage",level:2},{value:"Manual transaction management",id:"manual-transaction-management",level:3},{value:"Specifying transaction mode and isolation level",id:"specifying-transaction-mode-and-isolation-level",level:3},{value:"Unit of work lifecycle states",id:"unit-of-work-lifecycle-states",level:3},{value:"Pipeline-based unit of work (MediatR)",id:"pipeline-based-unit-of-work-mediatr",level:3},{value:"API Summary",id:"api-summary",level:2}];function h(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,o.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"unit-of-work",children:"Unit of Work"}),"\n",(0,i.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsxs)(n.p,{children:["The Unit of Work pattern coordinates writes to one or more repositories within a single transaction boundary. In RCommon the unit of work wraps a ",(0,i.jsx)(n.code,{children:"System.Transactions.TransactionScope"}),", which means it can span multiple data stores without requiring distributed transaction infrastructure in most scenarios."]}),"\n",(0,i.jsx)(n.p,{children:"The typical workflow is:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["Create a unit of work via ",(0,i.jsx)(n.code,{children:"IUnitOfWorkFactory"}),"."]}),"\n",(0,i.jsx)(n.li,{children:"Execute repository operations (add, update, delete) as normal."}),"\n",(0,i.jsxs)(n.li,{children:["Call ",(0,i.jsx)(n.code,{children:"CommitAsync"})," to commit; or dispose without committing to roll back."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["RCommon also integrates with MediatR and Wolverine pipelines so that units of work can be opened and committed automatically around command handlers \u2014 see ",(0,i.jsx)(n.a,{href:"../cqrs-mediator/mediatr",children:"MediatR integration"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(r.A,{packageName:"RCommon.Persistence"}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["Configure the unit of work in your application startup using ",(0,i.jsx)(n.code,{children:"WithUnitOfWork"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithUnitOfWork(unitOfWork =>\r\n {\r\n unitOfWork.SetOptions(options =>\r\n {\r\n options.AutoCompleteScope = true; // commit on dispose if not already committed\r\n options.DefaultIsolation = IsolationLevel.ReadCommitted;\r\n });\r\n });\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"UnitOfWorkSettings"})," defaults:"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Setting"}),(0,i.jsx)(n.th,{children:"Default"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"DefaultIsolation"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ReadCommitted"})}),(0,i.jsxs)(n.td,{children:["The isolation level for new ",(0,i.jsx)(n.code,{children:"TransactionScope"})," instances"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"AutoCompleteScope"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"false"})}),(0,i.jsxs)(n.td,{children:["When ",(0,i.jsx)(n.code,{children:"true"}),", the scope is completed on disposal if no explicit commit or rollback occurred"]})]})]})]}),"\n",(0,i.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(n.h3,{id:"manual-transaction-management",children:"Manual transaction management"}),"\n",(0,i.jsxs)(n.p,{children:["Inject ",(0,i.jsx)(n.code,{children:"IUnitOfWorkFactory"})," and create a unit of work around your operations:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class TransferService\r\n{\r\n private readonly IUnitOfWorkFactory _uowFactory;\r\n private readonly IGraphRepository _accounts;\r\n\r\n public TransferService(IUnitOfWorkFactory uowFactory, IGraphRepository accounts)\r\n {\r\n _uowFactory = uowFactory;\r\n _accounts = accounts;\r\n }\r\n\r\n public async Task TransferAsync(Guid fromId, Guid toId, decimal amount,\r\n CancellationToken cancellationToken)\r\n {\r\n using IUnitOfWork uow = _uowFactory.Create();\r\n\r\n Account from = await _accounts.FindAsync(fromId, cancellationToken);\r\n Account to = await _accounts.FindAsync(toId, cancellationToken);\r\n\r\n from.Debit(amount);\r\n to.Credit(amount);\r\n\r\n await _accounts.UpdateAsync(from, cancellationToken);\r\n await _accounts.UpdateAsync(to, cancellationToken);\r\n\r\n await uow.CommitAsync(cancellationToken);\r\n }\r\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Disposing the ",(0,i.jsx)(n.code,{children:"IUnitOfWork"})," without calling ",(0,i.jsx)(n.code,{children:"CommitAsync"})," rolls back the transaction."]}),"\n",(0,i.jsx)(n.h3,{id:"specifying-transaction-mode-and-isolation-level",children:"Specifying transaction mode and isolation level"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"IUnitOfWorkFactory.Create"})," has overloads for controlling how the unit of work participates in ambient transactions:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"// Default settings from configuration\r\nIUnitOfWork uow = _uowFactory.Create();\r\n\r\n// Explicit transaction mode\r\nIUnitOfWork uow = _uowFactory.Create(TransactionMode.New);\r\n\r\n// Explicit mode and isolation level\r\nIUnitOfWork uow = _uowFactory.Create(TransactionMode.New, IsolationLevel.Serializable);\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"TransactionMode"})," maps to the ",(0,i.jsx)(n.code,{children:"TransactionScopeOption"})," used when creating the ",(0,i.jsx)(n.code,{children:"TransactionScope"}),":"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Mode"}),(0,i.jsx)(n.th,{children:"Behaviour"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Default"})}),(0,i.jsxs)(n.td,{children:["Uses ",(0,i.jsx)(n.code,{children:"TransactionScopeOption.Required"})," \u2014 joins an ambient transaction if one exists"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"New"})}),(0,i.jsxs)(n.td,{children:["Uses ",(0,i.jsx)(n.code,{children:"TransactionScopeOption.RequiresNew"})," \u2014 always creates a new transaction"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Suppress"})}),(0,i.jsxs)(n.td,{children:["Uses ",(0,i.jsx)(n.code,{children:"TransactionScopeOption.Suppress"})," \u2014 executes outside any ambient transaction"]})]})]})]}),"\n",(0,i.jsx)(n.h3,{id:"unit-of-work-lifecycle-states",children:"Unit of work lifecycle states"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"IUnitOfWork.State"})," reports the current lifecycle stage:"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"State"}),(0,i.jsx)(n.th,{children:"Meaning"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Created"})}),(0,i.jsx)(n.td,{children:"Newly created; no commit or rollback has been attempted"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"CommitAttempted"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"CommitAsync"})," has been called"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Completed"})}),(0,i.jsx)(n.td,{children:"Successfully committed"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RolledBack"})}),(0,i.jsx)(n.td,{children:"The transaction was rolled back"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Disposed"})}),(0,i.jsx)(n.td,{children:"The unit of work has been disposed and cannot be reused"})]})]})]}),"\n",(0,i.jsx)(n.h3,{id:"pipeline-based-unit-of-work-mediatr",children:"Pipeline-based unit of work (MediatR)"}),"\n",(0,i.jsx)(n.p,{children:"In the CleanWithCQRS example the unit of work is applied to every command handler automatically through the MediatR pipeline:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithMediator(mediator =>\r\n {\r\n mediator.AddUnitOfWorkToRequestPipeline(); // wraps each handler in a unit of work\r\n })\r\n .WithUnitOfWork(unitOfWork =>\r\n {\r\n unitOfWork.SetOptions(options =>\r\n {\r\n options.AutoCompleteScope = true;\r\n options.DefaultIsolation = IsolationLevel.ReadCommitted;\r\n });\r\n });\n"})}),"\n",(0,i.jsxs)(n.p,{children:["With ",(0,i.jsx)(n.code,{children:"AutoCompleteScope = true"})," the unit of work commits automatically when the pipeline behavior disposes it after a successful handler execution."]}),"\n",(0,i.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Purpose"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IUnitOfWorkFactory"})}),(0,i.jsxs)(n.td,{children:["Creates ",(0,i.jsx)(n.code,{children:"IUnitOfWork"})," instances with default or explicit transaction settings"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IUnitOfWork"})}),(0,i.jsxs)(n.td,{children:["Manages the transaction: exposes ",(0,i.jsx)(n.code,{children:"CommitAsync()"}),", ",(0,i.jsx)(n.code,{children:"State"}),", ",(0,i.jsx)(n.code,{children:"AutoComplete"}),", ",(0,i.jsx)(n.code,{children:"IsolationLevel"}),", ",(0,i.jsx)(n.code,{children:"TransactionMode"}),", ",(0,i.jsx)(n.code,{children:"TransactionId"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IUnitOfWorkBuilder"})}),(0,i.jsxs)(n.td,{children:["Fluent startup builder \u2014 call ",(0,i.jsx)(n.code,{children:"SetOptions(Action)"})," to configure defaults"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"UnitOfWorkSettings"})}),(0,i.jsxs)(n.td,{children:["Holds ",(0,i.jsx)(n.code,{children:"DefaultIsolation"})," and ",(0,i.jsx)(n.code,{children:"AutoCompleteScope"})," defaults"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"UnitOfWorkState"})}),(0,i.jsxs)(n.td,{children:["Enum: ",(0,i.jsx)(n.code,{children:"Created"}),", ",(0,i.jsx)(n.code,{children:"CommitAttempted"}),", ",(0,i.jsx)(n.code,{children:"Completed"}),", ",(0,i.jsx)(n.code,{children:"RolledBack"}),", ",(0,i.jsx)(n.code,{children:"Disposed"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"TransactionMode"})}),(0,i.jsxs)(n.td,{children:["Enum: ",(0,i.jsx)(n.code,{children:"Default"}),", ",(0,i.jsx)(n.code,{children:"New"}),", ",(0,i.jsx)(n.code,{children:"Suppress"})]})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},75783(e,n,t){t.d(n,{A:()=>l});var i=t(30758);const o="container_xjrG",r="label_Y4p8",s="commandRow_FY5I",c="command_m7Qs",a="copyButton_u1GK";var d=t(86070);function l({packageName:e,version:n}){const[t,l]=(0,i.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,d.jsxs)("div",{className:o,children:[(0,d.jsx)("div",{className:r,children:"NuGet Package"}),(0,d.jsxs)("div",{className:s,children:[(0,d.jsx)("code",{className:c,children:h}),(0,d.jsx)("button",{className:a,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:t?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,t){t.d(n,{R:()=>s,x:()=>c});var i=t(30758);const o={},r=i.createContext(o);function s(e){const n=i.useContext(r);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:s(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/6d12448e.9b29942f.js b/website/build/assets/js/6d12448e.9b29942f.js new file mode 100644 index 00000000..19b0d639 --- /dev/null +++ b/website/build/assets/js/6d12448e.9b29942f.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[5949],{78422(e,n,t){t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>c,default:()=>u,frontMatter:()=>r,metadata:()=>a,toc:()=>l});var i=t(86070),o=t(81753),s=t(75783);const r={title:"Unit of Work",sidebar_position:3,description:"Coordinate writes across multiple repositories in a single TransactionScope using RCommon's IUnitOfWorkFactory, with MediatR and Wolverine pipeline integration support."},c="Unit of Work",a={id:"persistence/unit-of-work",title:"Unit of Work",description:"Coordinate writes across multiple repositories in a single TransactionScope using RCommon's IUnitOfWorkFactory, with MediatR and Wolverine pipeline integration support.",source:"@site/docs/persistence/unit-of-work.mdx",sourceDirName:"persistence",slug:"/persistence/unit-of-work",permalink:"/docs/next/persistence/unit-of-work",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/persistence/unit-of-work.mdx",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"Unit of Work",sidebar_position:3,description:"Coordinate writes across multiple repositories in a single TransactionScope using RCommon's IUnitOfWorkFactory, with MediatR and Wolverine pipeline integration support."},sidebar:"docsSidebar",previous:{title:"Specifications",permalink:"/docs/next/persistence/specifications"},next:{title:"Entity Framework Core",permalink:"/docs/next/persistence/efcore"}},d={},l=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Usage",id:"usage",level:2},{value:"Manual transaction management",id:"manual-transaction-management",level:3},{value:"Specifying transaction mode and isolation level",id:"specifying-transaction-mode-and-isolation-level",level:3},{value:"Unit of work lifecycle states",id:"unit-of-work-lifecycle-states",level:3},{value:"Pipeline-based unit of work (MediatR)",id:"pipeline-based-unit-of-work-mediatr",level:3},{value:"API Summary",id:"api-summary",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,o.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"unit-of-work",children:"Unit of Work"}),"\n",(0,i.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsxs)(n.p,{children:["The Unit of Work pattern coordinates writes to one or more repositories within a single transaction boundary. In RCommon the unit of work wraps a ",(0,i.jsx)(n.code,{children:"System.Transactions.TransactionScope"}),", which means it can span multiple data stores without requiring distributed transaction infrastructure in most scenarios."]}),"\n",(0,i.jsx)(n.p,{children:"The typical workflow is:"}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["Create a unit of work via ",(0,i.jsx)(n.code,{children:"IUnitOfWorkFactory"}),"."]}),"\n",(0,i.jsx)(n.li,{children:"Execute repository operations (add, update, delete) as normal."}),"\n",(0,i.jsxs)(n.li,{children:["Call ",(0,i.jsx)(n.code,{children:"CommitAsync"})," to commit; or dispose without committing to roll back."]}),"\n"]}),"\n",(0,i.jsxs)(n.p,{children:["RCommon also integrates with MediatR and Wolverine pipelines so that units of work can be opened and committed automatically around command handlers \u2014 see ",(0,i.jsx)(n.a,{href:"../cqrs-mediator/mediatr",children:"MediatR integration"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(s.A,{packageName:"RCommon.Persistence"}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["Configure the unit of work in your application startup using ",(0,i.jsx)(n.code,{children:"WithUnitOfWork"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithUnitOfWork(unitOfWork =>\n {\n unitOfWork.SetOptions(options =>\n {\n options.AutoCompleteScope = true; // commit on dispose if not already committed\n options.DefaultIsolation = IsolationLevel.ReadCommitted;\n });\n });\n"})}),"\n",(0,i.jsx)(n.admonition,{title:"Modular composition",type:"tip",children:(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"WithUnitOfWork"})," is cached by concrete builder type. When two modules both call ",(0,i.jsx)(n.code,{children:"WithUnitOfWork(...)"}),", the second call reuses the cached sub-builder and runs its ",(0,i.jsx)(n.code,{children:"SetOptions"})," delegate against the same instance \u2014 last-write-wins for option values, but the builder is constructed exactly once. See ",(0,i.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"UnitOfWorkSettings"})," defaults:"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Setting"}),(0,i.jsx)(n.th,{children:"Default"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"DefaultIsolation"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ReadCommitted"})}),(0,i.jsxs)(n.td,{children:["The isolation level for new ",(0,i.jsx)(n.code,{children:"TransactionScope"})," instances"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"AutoCompleteScope"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"false"})}),(0,i.jsxs)(n.td,{children:["When ",(0,i.jsx)(n.code,{children:"true"}),", the scope is completed on disposal if no explicit commit or rollback occurred"]})]})]})]}),"\n",(0,i.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(n.h3,{id:"manual-transaction-management",children:"Manual transaction management"}),"\n",(0,i.jsxs)(n.p,{children:["Inject ",(0,i.jsx)(n.code,{children:"IUnitOfWorkFactory"})," and create a unit of work around your operations:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class TransferService\n{\n private readonly IUnitOfWorkFactory _uowFactory;\n private readonly IGraphRepository _accounts;\n\n public TransferService(IUnitOfWorkFactory uowFactory, IGraphRepository accounts)\n {\n _uowFactory = uowFactory;\n _accounts = accounts;\n }\n\n public async Task TransferAsync(Guid fromId, Guid toId, decimal amount,\n CancellationToken cancellationToken)\n {\n using IUnitOfWork uow = _uowFactory.Create();\n\n Account from = await _accounts.FindAsync(fromId, cancellationToken);\n Account to = await _accounts.FindAsync(toId, cancellationToken);\n\n from.Debit(amount);\n to.Credit(amount);\n\n await _accounts.UpdateAsync(from, cancellationToken);\n await _accounts.UpdateAsync(to, cancellationToken);\n\n await uow.CommitAsync(cancellationToken);\n }\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Disposing the ",(0,i.jsx)(n.code,{children:"IUnitOfWork"})," without calling ",(0,i.jsx)(n.code,{children:"CommitAsync"})," rolls back the transaction."]}),"\n",(0,i.jsx)(n.h3,{id:"specifying-transaction-mode-and-isolation-level",children:"Specifying transaction mode and isolation level"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"IUnitOfWorkFactory.Create"})," has overloads for controlling how the unit of work participates in ambient transactions:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"// Default settings from configuration\nIUnitOfWork uow = _uowFactory.Create();\n\n// Explicit transaction mode\nIUnitOfWork uow = _uowFactory.Create(TransactionMode.New);\n\n// Explicit mode and isolation level\nIUnitOfWork uow = _uowFactory.Create(TransactionMode.New, IsolationLevel.Serializable);\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"TransactionMode"})," maps to the ",(0,i.jsx)(n.code,{children:"TransactionScopeOption"})," used when creating the ",(0,i.jsx)(n.code,{children:"TransactionScope"}),":"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Mode"}),(0,i.jsx)(n.th,{children:"Behaviour"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Default"})}),(0,i.jsxs)(n.td,{children:["Uses ",(0,i.jsx)(n.code,{children:"TransactionScopeOption.Required"})," \u2014 joins an ambient transaction if one exists"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"New"})}),(0,i.jsxs)(n.td,{children:["Uses ",(0,i.jsx)(n.code,{children:"TransactionScopeOption.RequiresNew"})," \u2014 always creates a new transaction"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Suppress"})}),(0,i.jsxs)(n.td,{children:["Uses ",(0,i.jsx)(n.code,{children:"TransactionScopeOption.Suppress"})," \u2014 executes outside any ambient transaction"]})]})]})]}),"\n",(0,i.jsx)(n.h3,{id:"unit-of-work-lifecycle-states",children:"Unit of work lifecycle states"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"IUnitOfWork.State"})," reports the current lifecycle stage:"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"State"}),(0,i.jsx)(n.th,{children:"Meaning"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Created"})}),(0,i.jsx)(n.td,{children:"Newly created; no commit or rollback has been attempted"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"CommitAttempted"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"CommitAsync"})," has been called"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Completed"})}),(0,i.jsx)(n.td,{children:"Successfully committed"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"RolledBack"})}),(0,i.jsx)(n.td,{children:"The transaction was rolled back"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Disposed"})}),(0,i.jsx)(n.td,{children:"The unit of work has been disposed and cannot be reused"})]})]})]}),"\n",(0,i.jsx)(n.h3,{id:"pipeline-based-unit-of-work-mediatr",children:"Pipeline-based unit of work (MediatR)"}),"\n",(0,i.jsx)(n.p,{children:"In the CleanWithCQRS example the unit of work is applied to every command handler automatically through the MediatR pipeline:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithMediator(mediator =>\n {\n mediator.AddUnitOfWorkToRequestPipeline(); // wraps each handler in a unit of work\n })\n .WithUnitOfWork(unitOfWork =>\n {\n unitOfWork.SetOptions(options =>\n {\n options.AutoCompleteScope = true;\n options.DefaultIsolation = IsolationLevel.ReadCommitted;\n });\n });\n"})}),"\n",(0,i.jsxs)(n.p,{children:["With ",(0,i.jsx)(n.code,{children:"AutoCompleteScope = true"})," the unit of work commits automatically when the pipeline behavior disposes it after a successful handler execution."]}),"\n",(0,i.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Purpose"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IUnitOfWorkFactory"})}),(0,i.jsxs)(n.td,{children:["Creates ",(0,i.jsx)(n.code,{children:"IUnitOfWork"})," instances with default or explicit transaction settings"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IUnitOfWork"})}),(0,i.jsxs)(n.td,{children:["Manages the transaction: exposes ",(0,i.jsx)(n.code,{children:"CommitAsync()"}),", ",(0,i.jsx)(n.code,{children:"State"}),", ",(0,i.jsx)(n.code,{children:"AutoComplete"}),", ",(0,i.jsx)(n.code,{children:"IsolationLevel"}),", ",(0,i.jsx)(n.code,{children:"TransactionMode"}),", ",(0,i.jsx)(n.code,{children:"TransactionId"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IUnitOfWorkBuilder"})}),(0,i.jsxs)(n.td,{children:["Fluent startup builder \u2014 call ",(0,i.jsx)(n.code,{children:"SetOptions(Action)"})," to configure defaults"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"UnitOfWorkSettings"})}),(0,i.jsxs)(n.td,{children:["Holds ",(0,i.jsx)(n.code,{children:"DefaultIsolation"})," and ",(0,i.jsx)(n.code,{children:"AutoCompleteScope"})," defaults"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"UnitOfWorkState"})}),(0,i.jsxs)(n.td,{children:["Enum: ",(0,i.jsx)(n.code,{children:"Created"}),", ",(0,i.jsx)(n.code,{children:"CommitAttempted"}),", ",(0,i.jsx)(n.code,{children:"Completed"}),", ",(0,i.jsx)(n.code,{children:"RolledBack"}),", ",(0,i.jsx)(n.code,{children:"Disposed"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"TransactionMode"})}),(0,i.jsxs)(n.td,{children:["Enum: ",(0,i.jsx)(n.code,{children:"Default"}),", ",(0,i.jsx)(n.code,{children:"New"}),", ",(0,i.jsx)(n.code,{children:"Suppress"})]})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},75783(e,n,t){t.d(n,{A:()=>l});var i=t(30758);const o="container_xjrG",s="label_Y4p8",r="commandRow_FY5I",c="command_m7Qs",a="copyButton_u1GK";var d=t(86070);function l({packageName:e,version:n}){const[t,l]=(0,i.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,d.jsxs)("div",{className:o,children:[(0,d.jsx)("div",{className:s,children:"NuGet Package"}),(0,d.jsxs)("div",{className:r,children:[(0,d.jsx)("code",{className:c,children:h}),(0,d.jsx)("button",{className:a,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:t?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,t){t.d(n,{R:()=>r,x:()=>c});var i=t(30758);const o={},s=i.createContext(o);function r(e){const n=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/706a23ff.6f429037.js b/website/build/assets/js/706a23ff.c533cbeb.js similarity index 71% rename from website/build/assets/js/706a23ff.6f429037.js rename to website/build/assets/js/706a23ff.c533cbeb.js index 0d3e3723..9039c4e7 100644 --- a/website/build/assets/js/706a23ff.6f429037.js +++ b/website/build/assets/js/706a23ff.c533cbeb.js @@ -1 +1 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[5088],{45575(e,i,n){n.r(i),n.d(i,{assets:()=>d,contentTitle:()=>s,default:()=>h,frontMatter:()=>r,metadata:()=>c,toc:()=>a});var o=n(86070),t=n(81753);const r={title:"Overview & Philosophy",sidebar_position:1},s="Overview & Philosophy",c={id:"getting-started/overview",title:"Overview & Philosophy",description:"RCommon is a .NET infrastructure library that provides battle-tested abstractions for the concerns common to every production application: persistence, CQRS/mediator, event handling, caching, validation, emailing, and more.",source:"@site/versioned_docs/version-2.4.1/getting-started/overview.mdx",sourceDirName:"getting-started",slug:"/getting-started/overview",permalink:"/docs/getting-started/overview",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/versioned_docs/version-2.4.1/getting-started/overview.mdx",tags:[],version:"2.4.1",sidebarPosition:1,frontMatter:{title:"Overview & Philosophy",sidebar_position:1},sidebar:"docsSidebar",previous:{title:"Getting Started",permalink:"/docs/category/getting-started"},next:{title:"Installation",permalink:"/docs/getting-started/installation"}},d={},a=[{value:"What RCommon provides",id:"what-rcommon-provides",level:2},{value:"What RCommon is not",id:"what-rcommon-is-not",level:2},{value:"Supported frameworks",id:"supported-frameworks",level:2},{value:"License",id:"license",level:2},{value:"Source code",id:"source-code",level:2},{value:"Design principles",id:"design-principles",level:2}];function l(e){const i={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",strong:"strong",ul:"ul",...(0,t.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(i.h1,{id:"overview--philosophy",children:"Overview & Philosophy"}),"\n",(0,o.jsx)(i.p,{children:"RCommon is a .NET infrastructure library that provides battle-tested abstractions for the concerns common to every production application: persistence, CQRS/mediator, event handling, caching, validation, emailing, and more."}),"\n",(0,o.jsx)(i.p,{children:"The goal is not to invent new patterns but to give you clean, consistent interfaces over the implementations you already use \u2014 Entity Framework Core, MediatR, MassTransit, Wolverine, Redis, and others \u2014 so that swapping providers never touches your domain or application code."}),"\n",(0,o.jsx)(i.h2,{id:"what-rcommon-provides",children:"What RCommon provides"}),"\n",(0,o.jsxs)(i.ul,{children:["\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.strong,{children:"Fluent, DI-first configuration"})," \u2014 a single ",(0,o.jsx)(i.code,{children:"AddRCommon()"})," call bootstraps everything through a composable builder chain."]}),"\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.strong,{children:"Pluggable providers"})," \u2014 every feature area exposes a generic slot (",(0,o.jsx)(i.code,{children:"WithPersistence"}),", ",(0,o.jsx)(i.code,{children:"WithMediator"}),", ",(0,o.jsx)(i.code,{children:"WithEventHandling"}),", etc.) that accepts whichever implementation package you install."]}),"\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.strong,{children:"Repository and unit-of-work abstractions"})," \u2014 ",(0,o.jsx)(i.code,{children:"ILinqRepository"}),", ",(0,o.jsx)(i.code,{children:"IReadOnlyRepository"}),", ",(0,o.jsx)(i.code,{children:"IWriteOnlyRepository"}),", and ",(0,o.jsx)(i.code,{children:"IAggregateRepository"})," sit in front of EF Core, Dapper, or Linq2Db."]}),"\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.strong,{children:"Domain-driven design primitives"})," \u2014 base entity classes with auditing, soft delete, and transactional domain events built in."]}),"\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.strong,{children:"In-process event bus"})," \u2014 ",(0,o.jsx)(i.code,{children:"IEventBus"})," with subscriber registration and a transactional ",(0,o.jsx)(i.code,{children:"IEventRouter"})," that coordinates domain events with the unit of work."]}),"\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.strong,{children:"CQRS mediation"})," \u2014 a thin ",(0,o.jsx)(i.code,{children:"IMediator"})," abstraction backed by MediatR or Wolverine."]}),"\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.strong,{children:"Utility types"})," \u2014 ",(0,o.jsx)(i.code,{children:"Guard"}),", ",(0,o.jsx)(i.code,{children:"ISystemTime"}),", ",(0,o.jsx)(i.code,{children:"IGuidGenerator"}),", ",(0,o.jsx)(i.code,{children:"ICommonFactory"}),", specification pattern support, and a rich set of extension methods."]}),"\n"]}),"\n",(0,o.jsx)(i.h2,{id:"what-rcommon-is-not",children:"What RCommon is not"}),"\n",(0,o.jsx)(i.p,{children:"RCommon does not dictate your architecture. It does not generate controllers, scaffold screens, impose a folder structure, or require you to inherit from framework base classes in your domain layer. You choose Clean Architecture, Vertical Slice, Modular Monolith, or anything else \u2014 RCommon works alongside whatever structure you adopt."}),"\n",(0,o.jsx)(i.h2,{id:"supported-frameworks",children:"Supported frameworks"}),"\n",(0,o.jsx)(i.p,{children:"RCommon targets:"}),"\n",(0,o.jsxs)(i.ul,{children:["\n",(0,o.jsx)(i.li,{children:".NET 8 (LTS)"}),"\n",(0,o.jsx)(i.li,{children:".NET 9"}),"\n",(0,o.jsx)(i.li,{children:".NET 10"}),"\n"]}),"\n",(0,o.jsx)(i.h2,{id:"license",children:"License"}),"\n",(0,o.jsxs)(i.p,{children:["RCommon is released under the ",(0,o.jsx)(i.a,{href:"https://www.apache.org/licenses/LICENSE-2.0",children:"Apache License, Version 2.0"}),". It is free for commercial and open-source use."]}),"\n",(0,o.jsx)(i.h2,{id:"source-code",children:"Source code"}),"\n",(0,o.jsxs)(i.p,{children:["The full source is available on GitHub: ",(0,o.jsx)(i.a,{href:"https://github.com/RCommon-Framework/RCommon",children:"https://github.com/jfrog/RCommon"})]}),"\n",(0,o.jsx)(i.h2,{id:"design-principles",children:"Design principles"}),"\n",(0,o.jsxs)(i.p,{children:[(0,o.jsx)(i.strong,{children:"Abstractions over implementations."})," Your application code depends on ",(0,o.jsx)(i.code,{children:"ILinqRepository"}),", not ",(0,o.jsx)(i.code,{children:"EFCoreRepository"}),". The concrete type is registered at the composition root and can be swapped without touching business logic."]}),"\n",(0,o.jsxs)(i.p,{children:[(0,o.jsx)(i.strong,{children:"Fluent composition over convention magic."})," Every feature is opt-in. If you do not call ",(0,o.jsx)(i.code,{children:"WithPersistence()"}),", no persistence services are registered. There is no scanning, no implicit wiring, and no surprises."]}),"\n",(0,o.jsxs)(i.p,{children:[(0,o.jsx)(i.strong,{children:"Single entry point."})," ",(0,o.jsx)(i.code,{children:"services.AddRCommon()"})," returns an ",(0,o.jsx)(i.code,{children:"IRCommonBuilder"}),". From there, every subsystem is a method call on that builder. The chain is self-documenting and IDE-discoverable."]}),"\n",(0,o.jsxs)(i.p,{children:[(0,o.jsx)(i.strong,{children:"Testability."})," Abstractions like ",(0,o.jsx)(i.code,{children:"ISystemTime"})," and ",(0,o.jsx)(i.code,{children:"IGuidGenerator"})," exist specifically so that deterministic unit tests are straightforward without mocking static members or ",(0,o.jsx)(i.code,{children:"DateTime.Now"}),"."]})]})}function h(e={}){const{wrapper:i}={...(0,t.R)(),...e.components};return i?(0,o.jsx)(i,{...e,children:(0,o.jsx)(l,{...e})}):l(e)}},81753(e,i,n){n.d(i,{R:()=>s,x:()=>c});var o=n(30758);const t={},r=o.createContext(t);function s(e){const i=o.useContext(r);return o.useMemo(function(){return"function"==typeof e?e(i):{...i,...e}},[i,e])}function c(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:s(e.components),o.createElement(r.Provider,{value:i},e.children)}}}]); \ No newline at end of file +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[5088],{45575(e,i,n){n.r(i),n.d(i,{assets:()=>a,contentTitle:()=>s,default:()=>h,frontMatter:()=>r,metadata:()=>c,toc:()=>d});var o=n(86070),t=n(81753);const r={title:"Overview & Philosophy",sidebar_position:1},s="Overview & Philosophy",c={id:"getting-started/overview",title:"Overview & Philosophy",description:"RCommon is a .NET infrastructure library that provides battle-tested abstractions for the concerns common to every production application: persistence, CQRS/mediator, event handling, caching, validation, emailing, and more.",source:"@site/versioned_docs/version-2.4.1/getting-started/overview.mdx",sourceDirName:"getting-started",slug:"/getting-started/overview",permalink:"/docs/getting-started/overview",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/versioned_docs/version-2.4.1/getting-started/overview.mdx",tags:[],version:"2.4.1",sidebarPosition:1,frontMatter:{title:"Overview & Philosophy",sidebar_position:1},sidebar:"docsSidebar",previous:{title:"Getting Started",permalink:"/docs/category/getting-started"},next:{title:"Installation",permalink:"/docs/getting-started/installation"}},a={},d=[{value:"What RCommon provides",id:"what-rcommon-provides",level:2},{value:"What RCommon is not",id:"what-rcommon-is-not",level:2},{value:"Supported frameworks",id:"supported-frameworks",level:2},{value:"License",id:"license",level:2},{value:"Source code",id:"source-code",level:2},{value:"Design principles",id:"design-principles",level:2}];function l(e){const i={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",p:"p",strong:"strong",ul:"ul",...(0,t.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(i.h1,{id:"overview--philosophy",children:"Overview & Philosophy"}),"\n",(0,o.jsx)(i.p,{children:"RCommon is a .NET infrastructure library that provides battle-tested abstractions for the concerns common to every production application: persistence, CQRS/mediator, event handling, caching, validation, emailing, and more."}),"\n",(0,o.jsx)(i.p,{children:"The goal is not to invent new patterns but to give you clean, consistent interfaces over the implementations you already use \u2014 Entity Framework Core, MediatR, MassTransit, Wolverine, Redis, and others \u2014 so that swapping providers never touches your domain or application code."}),"\n",(0,o.jsx)(i.h2,{id:"what-rcommon-provides",children:"What RCommon provides"}),"\n",(0,o.jsxs)(i.ul,{children:["\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.strong,{children:"Fluent, DI-first configuration"})," \u2014 a single ",(0,o.jsx)(i.code,{children:"AddRCommon()"})," call bootstraps everything through a composable builder chain."]}),"\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.strong,{children:"Pluggable providers"})," \u2014 every feature area exposes a generic slot (",(0,o.jsx)(i.code,{children:"WithPersistence"}),", ",(0,o.jsx)(i.code,{children:"WithMediator"}),", ",(0,o.jsx)(i.code,{children:"WithEventHandling"}),", etc.) that accepts whichever implementation package you install."]}),"\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.strong,{children:"Repository and unit-of-work abstractions"})," \u2014 ",(0,o.jsx)(i.code,{children:"ILinqRepository"}),", ",(0,o.jsx)(i.code,{children:"IReadOnlyRepository"}),", ",(0,o.jsx)(i.code,{children:"IWriteOnlyRepository"}),", and ",(0,o.jsx)(i.code,{children:"IAggregateRepository"})," sit in front of EF Core, Dapper, or Linq2Db."]}),"\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.strong,{children:"Domain-driven design primitives"})," \u2014 base entity classes with auditing, soft delete, and transactional domain events built in."]}),"\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.strong,{children:"In-process event bus"})," \u2014 ",(0,o.jsx)(i.code,{children:"IEventBus"})," with subscriber registration and a transactional ",(0,o.jsx)(i.code,{children:"IEventRouter"})," that coordinates domain events with the unit of work."]}),"\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.strong,{children:"CQRS mediation"})," \u2014 a thin ",(0,o.jsx)(i.code,{children:"IMediator"})," abstraction backed by MediatR or Wolverine."]}),"\n",(0,o.jsxs)(i.li,{children:[(0,o.jsx)(i.strong,{children:"Utility types"})," \u2014 ",(0,o.jsx)(i.code,{children:"Guard"}),", ",(0,o.jsx)(i.code,{children:"ISystemTime"}),", ",(0,o.jsx)(i.code,{children:"IGuidGenerator"}),", ",(0,o.jsx)(i.code,{children:"ICommonFactory"}),", specification pattern support, and a rich set of extension methods."]}),"\n"]}),"\n",(0,o.jsx)(i.h2,{id:"what-rcommon-is-not",children:"What RCommon is not"}),"\n",(0,o.jsx)(i.p,{children:"RCommon does not dictate your architecture. It does not generate controllers, scaffold screens, impose a folder structure, or require you to inherit from framework base classes in your domain layer. You choose Clean Architecture, Vertical Slice, Modular Monolith, or anything else \u2014 RCommon works alongside whatever structure you adopt."}),"\n",(0,o.jsx)(i.h2,{id:"supported-frameworks",children:"Supported frameworks"}),"\n",(0,o.jsx)(i.p,{children:"RCommon targets:"}),"\n",(0,o.jsxs)(i.ul,{children:["\n",(0,o.jsx)(i.li,{children:".NET 8 (LTS)"}),"\n",(0,o.jsx)(i.li,{children:".NET 9"}),"\n",(0,o.jsx)(i.li,{children:".NET 10"}),"\n"]}),"\n",(0,o.jsx)(i.h2,{id:"license",children:"License"}),"\n",(0,o.jsxs)(i.p,{children:["RCommon is released under the ",(0,o.jsx)(i.a,{href:"https://www.apache.org/licenses/LICENSE-2.0",children:"Apache License, Version 2.0"}),". It is free for commercial and open-source use."]}),"\n",(0,o.jsx)(i.h2,{id:"source-code",children:"Source code"}),"\n",(0,o.jsxs)(i.p,{children:["The full source is available on GitHub: ",(0,o.jsx)(i.a,{href:"https://github.com/RCommon-Team/RCommon",children:"https://github.com/RCommon-Team/RCommon"})]}),"\n",(0,o.jsx)(i.h2,{id:"design-principles",children:"Design principles"}),"\n",(0,o.jsxs)(i.p,{children:[(0,o.jsx)(i.strong,{children:"Abstractions over implementations."})," Your application code depends on ",(0,o.jsx)(i.code,{children:"ILinqRepository"}),", not ",(0,o.jsx)(i.code,{children:"EFCoreRepository"}),". The concrete type is registered at the composition root and can be swapped without touching business logic."]}),"\n",(0,o.jsxs)(i.p,{children:[(0,o.jsx)(i.strong,{children:"Fluent composition over convention magic."})," Every feature is opt-in. If you do not call ",(0,o.jsx)(i.code,{children:"WithPersistence()"}),", no persistence services are registered. There is no scanning, no implicit wiring, and no surprises."]}),"\n",(0,o.jsxs)(i.p,{children:[(0,o.jsx)(i.strong,{children:"Single entry point."})," ",(0,o.jsx)(i.code,{children:"services.AddRCommon()"})," returns an ",(0,o.jsx)(i.code,{children:"IRCommonBuilder"}),". From there, every subsystem is a method call on that builder. The chain is self-documenting and IDE-discoverable."]}),"\n",(0,o.jsxs)(i.p,{children:[(0,o.jsx)(i.strong,{children:"Testability."})," Abstractions like ",(0,o.jsx)(i.code,{children:"ISystemTime"})," and ",(0,o.jsx)(i.code,{children:"IGuidGenerator"})," exist specifically so that deterministic unit tests are straightforward without mocking static members or ",(0,o.jsx)(i.code,{children:"DateTime.Now"}),"."]})]})}function h(e={}){const{wrapper:i}={...(0,t.R)(),...e.components};return i?(0,o.jsx)(i,{...e,children:(0,o.jsx)(l,{...e})}):l(e)}},81753(e,i,n){n.d(i,{R:()=>s,x:()=>c});var o=n(30758);const t={},r=o.createContext(t);function s(e){const i=o.useContext(r);return o.useMemo(function(){return"function"==typeof e?e(i):{...i,...e}},[i,e])}function c(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:s(e.components),o.createElement(r.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/72caafa9.5c7359ff.js b/website/build/assets/js/72caafa9.5c7359ff.js new file mode 100644 index 00000000..3f66e83e --- /dev/null +++ b/website/build/assets/js/72caafa9.5c7359ff.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[1375],{47232(e,r,n){n.r(r),n.d(r,{assets:()=>o,contentTitle:()=>d,default:()=>u,frontMatter:()=>c,metadata:()=>a,toc:()=>l});var i=n(86070),s=n(81753),t=n(75783);const c={title:"Authorization",sidebar_position:1},d="Authorization",a={id:"security-web/authorization",title:"Authorization",description:"Overview",source:"@site/versioned_docs/version-2.4.1/security-web/authorization.mdx",sourceDirName:"security-web",slug:"/security-web/authorization",permalink:"/docs/security-web/authorization",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/versioned_docs/version-2.4.1/security-web/authorization.mdx",tags:[],version:"2.4.1",sidebarPosition:1,frontMatter:{title:"Authorization",sidebar_position:1},sidebar:"docsSidebar",previous:{title:"Security & Web",permalink:"/docs/category/security--web"},next:{title:"Web Utilities",permalink:"/docs/security-web/web-utilities"}},o={},l=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Claims and principal accessor (non-web / background services)",id:"claims-and-principal-accessor-non-web--background-services",level:3},{value:"Claims and principal accessor (ASP.NET Core web apps)",id:"claims-and-principal-accessor-aspnet-core-web-apps",level:3},{value:"Overriding claim type URIs",id:"overriding-claim-type-uris",level:3},{value:"Swashbuckle operation filters",id:"swashbuckle-operation-filters",level:3},{value:"Usage",id:"usage",level:2},{value:"Accessing the current user",id:"accessing-the-current-user",level:3},{value:"Reading roles",id:"reading-roles",level:3},{value:"Reading arbitrary claims",id:"reading-arbitrary-claims",level:3},{value:"Accessing the current client",id:"accessing-the-current-client",level:3},{value:"Temporarily replacing the principal",id:"temporarily-replacing-the-principal",level:3},{value:"Signaling authorization failures",id:"signaling-authorization-failures",level:3},{value:"Claims identity helpers",id:"claims-identity-helpers",level:3},{value:"API Summary",id:"api-summary",level:2}];function h(e){const r={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(r.h1,{id:"authorization",children:"Authorization"}),"\n",(0,i.jsx)(r.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"RCommon.Security"})," provides the claims and principal accessor abstractions that RCommon uses internally for identity resolution. It does not replace ASP.NET Core's authorization pipeline \u2014 instead it sits alongside it, giving your application code a consistent way to read the current user's identity, roles, and claims without taking a hard dependency on ",(0,i.jsx)(r.code,{children:"HttpContext"})," or ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"}),"."]}),"\n",(0,i.jsx)(r.p,{children:"The core design is a layered stack:"}),"\n",(0,i.jsxs)(r.ul,{children:["\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})," \u2014 retrieves the current ",(0,i.jsx)(r.code,{children:"ClaimsPrincipal"})," and supports temporarily replacing it for a scoped context (useful in background workers and tests)."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentUser"})," \u2014 reads well-known identity properties (ID, roles, tenant, arbitrary claims) from whatever principal the accessor provides."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentClient"})," \u2014 reads the OAuth client ID claim from the same principal."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ClaimTypesConst"})," \u2014 a static class of configurable claim type URIs so that you can remap standard claims to the values your identity provider actually issues."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"AuthorizationException"})," \u2014 a structured exception type for signaling authorization failures through the application tier."]}),"\n"]}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"RCommon.Authorization.Web"})," adds two Swashbuckle operation filters that keep your OpenAPI/Swagger documentation accurate when your API uses authorization:"]}),"\n",(0,i.jsxs)(r.ul,{children:["\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"AuthorizeCheckOperationFilter"})," \u2014 detects ",(0,i.jsx)(r.code,{children:"[Authorize]"})," on controllers and actions and adds 401/403 responses plus an OAuth2 security requirement to the generated operation."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"AuthorizationHeaderParameterOperationFilter"})," \u2014 detects ",(0,i.jsx)(r.code,{children:"AuthorizeFilter"})," in the MVC filter pipeline and adds a required ",(0,i.jsx)(r.code,{children:"Authorization"})," header parameter to the operation so that Swagger UI can accept a bearer token."]}),"\n"]}),"\n",(0,i.jsx)(r.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(r.p,{children:"For the core security abstractions (principal accessor, current user, claims helpers):"}),"\n",(0,i.jsx)(t.A,{packageName:"RCommon.Security"}),"\n",(0,i.jsx)(r.p,{children:"For ASP.NET Core web apps that need HTTP-context-aware principal resolution:"}),"\n",(0,i.jsx)(t.A,{packageName:"RCommon.Web"}),"\n",(0,i.jsx)(r.p,{children:"For the Swashbuckle/OpenAPI authorization filters:"}),"\n",(0,i.jsx)(t.A,{packageName:"RCommon.Authorization.Web"}),"\n",(0,i.jsx)(r.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsx)(r.h3,{id:"claims-and-principal-accessor-non-web--background-services",children:"Claims and principal accessor (non-web / background services)"}),"\n",(0,i.jsx)(r.p,{children:"Register the thread-based accessor when your application does not run inside ASP.NET Core request middleware:"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"using RCommon;\r\n\r\nbuilder.Services.AddRCommon(config =>\r\n{\r\n config.WithClaimsAndPrincipalAccessor();\r\n});\n"})}),"\n",(0,i.jsx)(r.p,{children:"This registers:"}),"\n",(0,i.jsxs)(r.ul,{children:["\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})," \u2192 ",(0,i.jsx)(r.code,{children:"ThreadCurrentPrincipalAccessor"})," (reads ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"}),")"]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentUser"})," \u2192 ",(0,i.jsx)(r.code,{children:"CurrentUser"})]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentClient"})," \u2192 ",(0,i.jsx)(r.code,{children:"CurrentClient"})]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ITenantIdAccessor"})," \u2192 ",(0,i.jsx)(r.code,{children:"ClaimsTenantIdAccessor"})]}),"\n"]}),"\n",(0,i.jsx)(r.h3,{id:"claims-and-principal-accessor-aspnet-core-web-apps",children:"Claims and principal accessor (ASP.NET Core web apps)"}),"\n",(0,i.jsxs)(r.p,{children:["Use the web variant which reads the principal from ",(0,i.jsx)(r.code,{children:"HttpContext.User"})," instead:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"using RCommon;\r\n\r\nbuilder.Services.AddRCommon(config =>\r\n{\r\n config.WithClaimsAndPrincipalAccessorForWeb();\r\n});\n"})}),"\n",(0,i.jsxs)(r.p,{children:["This registers the same interfaces but substitutes ",(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"})," for ",(0,i.jsx)(r.code,{children:"ThreadCurrentPrincipalAccessor"}),", and calls ",(0,i.jsx)(r.code,{children:"AddHttpContextAccessor()"})," automatically."]}),"\n",(0,i.jsx)(r.h3,{id:"overriding-claim-type-uris",children:"Overriding claim type URIs"}),"\n",(0,i.jsxs)(r.p,{children:["If your identity provider uses non-standard claim type URIs, override the static properties on ",(0,i.jsx)(r.code,{children:"ClaimTypesConst"})," once at startup before any requests are processed:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Claims;\r\n\r\n// Map to the short claim names issued by your OIDC provider.\r\n// Configure must be called once at startup, before any property is accessed.\r\nClaimTypesConst.Configure(options =>\r\n{\r\n options.UserId = "sub";\r\n options.Role = "roles";\r\n options.Email = "email";\r\n options.TenantId = "tenant_id";\r\n options.ClientId = "client_id";\r\n});\n'})}),"\n",(0,i.jsx)(r.h3,{id:"swashbuckle-operation-filters",children:"Swashbuckle operation filters"}),"\n",(0,i.jsx)(r.p,{children:"Add both filters to your Swagger generation options:"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Authorization.Web.Filters;\r\n\r\nbuilder.Services.AddSwaggerGen(options =>\r\n{\r\n options.OperationFilter();\r\n options.OperationFilter();\r\n\r\n // Register your OAuth2 security scheme so that the filters can reference it.\r\n options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme\r\n {\r\n Type = SecuritySchemeType.OAuth2,\r\n Flows = new OpenApiOAuthFlows\r\n {\r\n AuthorizationCode = new OpenApiOAuthFlow\r\n {\r\n AuthorizationUrl = new Uri("https://your-idp/connect/authorize"),\r\n TokenUrl = new Uri("https://your-idp/connect/token"),\r\n Scopes = new Dictionary { ["api"] = "API access" }\r\n }\r\n }\r\n });\r\n});\n'})}),"\n",(0,i.jsx)(r.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(r.h3,{id:"accessing-the-current-user",children:"Accessing the current user"}),"\n",(0,i.jsxs)(r.p,{children:["Inject ",(0,i.jsx)(r.code,{children:"ICurrentUser"})," wherever you need identity information:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Users;\r\n\r\npublic class OrderCommandHandler\r\n{\r\n private readonly ICurrentUser _currentUser;\r\n private readonly IGraphRepository _orders;\r\n\r\n public OrderCommandHandler(ICurrentUser currentUser, IGraphRepository orders)\r\n {\r\n _currentUser = currentUser;\r\n _orders = orders;\r\n }\r\n\r\n public async Task Handle(PlaceOrderCommand command, CancellationToken ct)\r\n {\r\n if (!_currentUser.IsAuthenticated)\r\n throw new AuthorizationException("You must be signed in to place an order.");\r\n\r\n var order = new Order\r\n {\r\n CustomerId = _currentUser.Id!,\r\n CustomerName = _currentUser.FindClaimValue(ClaimTypes.GivenName) ?? "Unknown",\r\n Total = command.Total\r\n };\r\n\r\n await _orders.AddAsync(order, ct);\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"reading-roles",children:"Reading roles"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'string[] roles = _currentUser.Roles; // distinct role values from ClaimTypesConst.Role claims\r\n\r\nif (!_currentUser.Roles.Contains("Administrator"))\r\n throw new AuthorizationException("Only administrators can perform this action.", "INSUFFICIENT_ROLE");\n'})}),"\n",(0,i.jsx)(r.h3,{id:"reading-arbitrary-claims",children:"Reading arbitrary claims"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'// Find the first matching claim.\r\nClaim? claim = _currentUser.FindClaim("custom:department");\r\n\r\n// Find all matching claims.\r\nClaim[] allDeptClaims = _currentUser.FindClaims("custom:department");\r\n\r\n// Find a claim value as a string.\r\nstring? department = _currentUser.FindClaimValue("custom:department");\n'})}),"\n",(0,i.jsx)(r.h3,{id:"accessing-the-current-client",children:"Accessing the current client"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Clients;\r\n\r\npublic class ApiAuditMiddleware\r\n{\r\n private readonly ICurrentClient _currentClient;\r\n\r\n public ApiAuditMiddleware(ICurrentClient currentClient)\r\n {\r\n _currentClient = currentClient;\r\n }\r\n\r\n public void LogRequest(string path)\r\n {\r\n if (_currentClient.IsAuthenticated)\r\n {\r\n Console.WriteLine($"Client {_currentClient.Id} called {path}");\r\n }\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"temporarily-replacing-the-principal",children:"Temporarily replacing the principal"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor.Change"})," replaces the current principal for the lifetime of the returned ",(0,i.jsx)(r.code,{children:"IDisposable"}),". This is useful in background jobs and integration tests:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Claims;\r\nusing System.Security.Claims;\r\n\r\npublic async Task RunAsServiceAccountAsync(\r\n ICurrentPrincipalAccessor principalAccessor,\r\n Func work)\r\n{\r\n var identity = new ClaimsIdentity(new[]\r\n {\r\n new Claim(ClaimTypesConst.UserId, Guid.NewGuid().ToString()),\r\n new Claim(ClaimTypesConst.Role, "ServiceAccount")\r\n }, "ServiceAccount");\r\n\r\n using (principalAccessor.Change(new ClaimsPrincipal(identity)))\r\n {\r\n await work();\r\n // Previous principal is restored when the using block exits.\r\n }\r\n}\n'})}),"\n",(0,i.jsxs)(r.p,{children:["The extension overloads let you pass a single ",(0,i.jsx)(r.code,{children:"Claim"}),", a collection of claims, or a ",(0,i.jsx)(r.code,{children:"ClaimsIdentity"})," directly:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using (principalAccessor.Change(new Claim(ClaimTypesConst.Role, "Tester")))\r\n{\r\n // ...\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"signaling-authorization-failures",children:"Signaling authorization failures"}),"\n",(0,i.jsxs)(r.p,{children:["Throw ",(0,i.jsx)(r.code,{children:"AuthorizationException"})," from application-layer code when a rule is violated. Global exception handlers or middleware can then translate it to an appropriate HTTP 403 response:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Authorization;\r\n\r\npublic void EnsureCanEdit(Document document)\r\n{\r\n if (document.OwnerId != _currentUser.Id)\r\n {\r\n throw new AuthorizationException(\r\n message: "You do not have permission to edit this document.",\r\n code: "DOCUMENT_ACCESS_DENIED")\r\n .WithData("DocumentId", document.Id)\r\n .WithData("UserId", _currentUser.Id);\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"claims-identity-helpers",children:"Claims identity helpers"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"ClaimsIdentityExtensions"})," provides fluent extension methods for managing claims:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security;\r\n\r\n// Add a claim only if one with the same type does not already exist.\r\nidentity.AddIfNotContains(new Claim("custom:role", "Editor"));\r\n\r\n// Remove all claims of the same type and set a new value.\r\nidentity.AddOrReplace(new Claim("custom:role", "Administrator"));\r\n\r\n// Add an identity to a principal only if the same authentication type is not already present.\r\nprincipal.AddIdentityIfNotContains(new ClaimsIdentity(claims, "Cookie"));\n'})}),"\n",(0,i.jsxs)(r.p,{children:["Extract well-known values directly from a ",(0,i.jsx)(r.code,{children:"ClaimsPrincipal"}),":"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"string? userId = principal.FindUserId(); // reads ClaimTypesConst.UserId\r\nstring? tenant = principal.FindTenantId(); // reads ClaimTypesConst.TenantId\r\nstring? client = principal.FindClientId(); // reads ClaimTypesConst.ClientId\n"})}),"\n",(0,i.jsx)(r.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(r.table,{children:[(0,i.jsx)(r.thead,{children:(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.th,{children:"Type"}),(0,i.jsx)(r.th,{children:"Package"}),(0,i.jsx)(r.th,{children:"Description"})]})}),(0,i.jsxs)(r.tbody,{children:[(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Provides the current ",(0,i.jsx)(r.code,{children:"ClaimsPrincipal"}),"; supports scoped override via ",(0,i.jsx)(r.code,{children:"Change()"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentPrincipalAccessorBase"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Abstract base implementing ",(0,i.jsx)(r.code,{children:"Change()"})," with ",(0,i.jsx)(r.code,{children:"AsyncLocal"})," storage"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ThreadCurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Default implementation; reads ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentPrincipalAccessorExtensions"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:[(0,i.jsx)(r.code,{children:"Change(Claim)"}),", ",(0,i.jsx)(r.code,{children:"Change(IEnumerable)"}),", ",(0,i.jsx)(r.code,{children:"Change(ClaimsIdentity)"})," overloads"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentUser"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Exposes ",(0,i.jsx)(r.code,{children:"Id"}),", ",(0,i.jsx)(r.code,{children:"IsAuthenticated"}),", ",(0,i.jsx)(r.code,{children:"Roles"}),", ",(0,i.jsx)(r.code,{children:"TenantId"}),", ",(0,i.jsx)(r.code,{children:"FindClaim"}),", ",(0,i.jsx)(r.code,{children:"FindClaims"}),", ",(0,i.jsx)(r.code,{children:"GetAllClaims"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentUser"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Default ",(0,i.jsx)(r.code,{children:"ICurrentUser"})," implementation backed by ",(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentUserExtensions"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:[(0,i.jsx)(r.code,{children:"FindClaimValue(string)"}),", ",(0,i.jsx)(r.code,{children:"GetId()"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentClient"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Exposes ",(0,i.jsx)(r.code,{children:"Id"})," and ",(0,i.jsx)(r.code,{children:"IsAuthenticated"})," for OAuth client identities"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentClient"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Default ",(0,i.jsx)(r.code,{children:"ICurrentClient"})," backed by ",(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ClaimTypesConst"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Configure-once claim type URIs via ",(0,i.jsx)(r.code,{children:"Configure(Action)"}),": ",(0,i.jsx)(r.code,{children:"UserName"}),", ",(0,i.jsx)(r.code,{children:"Name"}),", ",(0,i.jsx)(r.code,{children:"SurName"}),", ",(0,i.jsx)(r.code,{children:"UserId"}),", ",(0,i.jsx)(r.code,{children:"Role"}),", ",(0,i.jsx)(r.code,{children:"Email"}),", ",(0,i.jsx)(r.code,{children:"TenantId"}),", ",(0,i.jsx)(r.code,{children:"ClientId"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ClaimsIdentityExtensions"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:[(0,i.jsx)(r.code,{children:"FindUserId"}),", ",(0,i.jsx)(r.code,{children:"FindTenantId"}),", ",(0,i.jsx)(r.code,{children:"FindClientId"}),", ",(0,i.jsx)(r.code,{children:"AddIfNotContains"}),", ",(0,i.jsx)(r.code,{children:"AddOrReplace"}),", ",(0,i.jsx)(r.code,{children:"AddIdentityIfNotContains"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"AuthorizationException"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Application-layer exception for access-denied scenarios; carries ",(0,i.jsx)(r.code,{children:"Code"}),", ",(0,i.jsx)(r.code,{children:"LogLevel"}),", and fluent ",(0,i.jsx)(r.code,{children:"WithData()"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"AuthorizeCheckOperationFilter"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Authorization.Web"})}),(0,i.jsxs)(r.td,{children:["Swashbuckle filter: adds 401/403 responses and OAuth2 security requirement to ",(0,i.jsx)(r.code,{children:"[Authorize]"}),"-decorated operations"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"AuthorizationHeaderParameterOperationFilter"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Authorization.Web"})}),(0,i.jsxs)(r.td,{children:["Swashbuckle filter: adds a required ",(0,i.jsx)(r.code,{children:"Authorization"})," header parameter to operations protected by ",(0,i.jsx)(r.code,{children:"AuthorizeFilter"})]})]})]})]})]})}function u(e={}){const{wrapper:r}={...(0,s.R)(),...e.components};return r?(0,i.jsx)(r,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},75783(e,r,n){n.d(r,{A:()=>l});var i=n(30758);const s="container_xjrG",t="label_Y4p8",c="commandRow_FY5I",d="command_m7Qs",a="copyButton_u1GK";var o=n(86070);function l({packageName:e,version:r}){const[n,l]=(0,i.useState)(!1),h=r?`dotnet add package ${e} --version ${r}`:`dotnet add package ${e}`;return(0,o.jsxs)("div",{className:s,children:[(0,o.jsx)("div",{className:t,children:"NuGet Package"}),(0,o.jsxs)("div",{className:c,children:[(0,o.jsx)("code",{className:d,children:h}),(0,o.jsx)("button",{className:a,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:n?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,r,n){n.d(r,{R:()=>c,x:()=>d});var i=n(30758);const s={},t=i.createContext(s);function c(e){const r=i.useContext(t);return i.useMemo(function(){return"function"==typeof e?e(r):{...r,...e}},[r,e])}function d(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:c(e.components),i.createElement(t.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/72caafa9.a96c59ad.js b/website/build/assets/js/72caafa9.a96c59ad.js deleted file mode 100644 index 3ba6779c..00000000 --- a/website/build/assets/js/72caafa9.a96c59ad.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[1375],{47232(e,r,n){n.r(r),n.d(r,{assets:()=>o,contentTitle:()=>d,default:()=>u,frontMatter:()=>c,metadata:()=>a,toc:()=>l});var i=n(86070),s=n(81753),t=n(75783);const c={title:"Authorization",sidebar_position:1},d="Authorization",a={id:"security-web/authorization",title:"Authorization",description:"Overview",source:"@site/versioned_docs/version-2.4.1/security-web/authorization.mdx",sourceDirName:"security-web",slug:"/security-web/authorization",permalink:"/docs/security-web/authorization",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/versioned_docs/version-2.4.1/security-web/authorization.mdx",tags:[],version:"2.4.1",sidebarPosition:1,frontMatter:{title:"Authorization",sidebar_position:1},sidebar:"docsSidebar",previous:{title:"Security & Web",permalink:"/docs/category/security--web"},next:{title:"Web Utilities",permalink:"/docs/security-web/web-utilities"}},o={},l=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Claims and principal accessor (non-web / background services)",id:"claims-and-principal-accessor-non-web--background-services",level:3},{value:"Claims and principal accessor (ASP.NET Core web apps)",id:"claims-and-principal-accessor-aspnet-core-web-apps",level:3},{value:"Overriding claim type URIs",id:"overriding-claim-type-uris",level:3},{value:"Swashbuckle operation filters",id:"swashbuckle-operation-filters",level:3},{value:"Usage",id:"usage",level:2},{value:"Accessing the current user",id:"accessing-the-current-user",level:3},{value:"Reading roles",id:"reading-roles",level:3},{value:"Reading arbitrary claims",id:"reading-arbitrary-claims",level:3},{value:"Accessing the current client",id:"accessing-the-current-client",level:3},{value:"Temporarily replacing the principal",id:"temporarily-replacing-the-principal",level:3},{value:"Signaling authorization failures",id:"signaling-authorization-failures",level:3},{value:"Claims identity helpers",id:"claims-identity-helpers",level:3},{value:"API Summary",id:"api-summary",level:2}];function h(e){const r={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(r.h1,{id:"authorization",children:"Authorization"}),"\n",(0,i.jsx)(r.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"RCommon.Security"})," provides the claims and principal accessor abstractions that RCommon uses internally for identity resolution. It does not replace ASP.NET Core's authorization pipeline \u2014 instead it sits alongside it, giving your application code a consistent way to read the current user's identity, roles, and claims without taking a hard dependency on ",(0,i.jsx)(r.code,{children:"HttpContext"})," or ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"}),"."]}),"\n",(0,i.jsx)(r.p,{children:"The core design is a layered stack:"}),"\n",(0,i.jsxs)(r.ul,{children:["\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})," \u2014 retrieves the current ",(0,i.jsx)(r.code,{children:"ClaimsPrincipal"})," and supports temporarily replacing it for a scoped context (useful in background workers and tests)."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentUser"})," \u2014 reads well-known identity properties (ID, roles, tenant, arbitrary claims) from whatever principal the accessor provides."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentClient"})," \u2014 reads the OAuth client ID claim from the same principal."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ClaimTypesConst"})," \u2014 a static class of configurable claim type URIs so that you can remap standard claims to the values your identity provider actually issues."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"AuthorizationException"})," \u2014 a structured exception type for signaling authorization failures through the application tier."]}),"\n"]}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"RCommon.Authorization.Web"})," adds two Swashbuckle operation filters that keep your OpenAPI/Swagger documentation accurate when your API uses authorization:"]}),"\n",(0,i.jsxs)(r.ul,{children:["\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"AuthorizeCheckOperationFilter"})," \u2014 detects ",(0,i.jsx)(r.code,{children:"[Authorize]"})," on controllers and actions and adds 401/403 responses plus an OAuth2 security requirement to the generated operation."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"AuthorizationHeaderParameterOperationFilter"})," \u2014 detects ",(0,i.jsx)(r.code,{children:"AuthorizeFilter"})," in the MVC filter pipeline and adds a required ",(0,i.jsx)(r.code,{children:"Authorization"})," header parameter to the operation so that Swagger UI can accept a bearer token."]}),"\n"]}),"\n",(0,i.jsx)(r.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(r.p,{children:"For the core security abstractions (principal accessor, current user, claims helpers):"}),"\n",(0,i.jsx)(t.A,{packageName:"RCommon.Security"}),"\n",(0,i.jsx)(r.p,{children:"For ASP.NET Core web apps that need HTTP-context-aware principal resolution:"}),"\n",(0,i.jsx)(t.A,{packageName:"RCommon.Web"}),"\n",(0,i.jsx)(r.p,{children:"For the Swashbuckle/OpenAPI authorization filters:"}),"\n",(0,i.jsx)(t.A,{packageName:"RCommon.Authorization.Web"}),"\n",(0,i.jsx)(r.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsx)(r.h3,{id:"claims-and-principal-accessor-non-web--background-services",children:"Claims and principal accessor (non-web / background services)"}),"\n",(0,i.jsx)(r.p,{children:"Register the thread-based accessor when your application does not run inside ASP.NET Core request middleware:"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"using RCommon;\r\n\r\nbuilder.Services.AddRCommon(config =>\r\n{\r\n config.WithClaimsAndPrincipalAccessor();\r\n});\n"})}),"\n",(0,i.jsx)(r.p,{children:"This registers:"}),"\n",(0,i.jsxs)(r.ul,{children:["\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})," \u2192 ",(0,i.jsx)(r.code,{children:"ThreadCurrentPrincipalAccessor"})," (reads ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"}),")"]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentUser"})," \u2192 ",(0,i.jsx)(r.code,{children:"CurrentUser"})]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ICurrentClient"})," \u2192 ",(0,i.jsx)(r.code,{children:"CurrentClient"})]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"ITenantIdAccessor"})," \u2192 ",(0,i.jsx)(r.code,{children:"ClaimsTenantIdAccessor"})]}),"\n"]}),"\n",(0,i.jsx)(r.h3,{id:"claims-and-principal-accessor-aspnet-core-web-apps",children:"Claims and principal accessor (ASP.NET Core web apps)"}),"\n",(0,i.jsxs)(r.p,{children:["Use the web variant which reads the principal from ",(0,i.jsx)(r.code,{children:"HttpContext.User"})," instead:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"using RCommon;\r\n\r\nbuilder.Services.AddRCommon(config =>\r\n{\r\n config.WithClaimsAndPrincipalAccessorForWeb();\r\n});\n"})}),"\n",(0,i.jsxs)(r.p,{children:["This registers the same interfaces but substitutes ",(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"})," for ",(0,i.jsx)(r.code,{children:"ThreadCurrentPrincipalAccessor"}),", and calls ",(0,i.jsx)(r.code,{children:"AddHttpContextAccessor()"})," automatically."]}),"\n",(0,i.jsx)(r.h3,{id:"overriding-claim-type-uris",children:"Overriding claim type URIs"}),"\n",(0,i.jsxs)(r.p,{children:["If your identity provider uses non-standard claim type URIs, override the static properties on ",(0,i.jsx)(r.code,{children:"ClaimTypesConst"})," once at startup before any requests are processed:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Claims;\r\n\r\n// Map to the short claim names issued by your OIDC provider.\r\nClaimTypesConst.UserId = "sub";\r\nClaimTypesConst.Role = "roles";\r\nClaimTypesConst.Email = "email";\r\nClaimTypesConst.TenantId = "tenant_id";\r\nClaimTypesConst.ClientId = "client_id";\n'})}),"\n",(0,i.jsx)(r.h3,{id:"swashbuckle-operation-filters",children:"Swashbuckle operation filters"}),"\n",(0,i.jsx)(r.p,{children:"Add both filters to your Swagger generation options:"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Authorization.Web.Filters;\r\n\r\nbuilder.Services.AddSwaggerGen(options =>\r\n{\r\n options.OperationFilter();\r\n options.OperationFilter();\r\n\r\n // Register your OAuth2 security scheme so that the filters can reference it.\r\n options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme\r\n {\r\n Type = SecuritySchemeType.OAuth2,\r\n Flows = new OpenApiOAuthFlows\r\n {\r\n AuthorizationCode = new OpenApiOAuthFlow\r\n {\r\n AuthorizationUrl = new Uri("https://your-idp/connect/authorize"),\r\n TokenUrl = new Uri("https://your-idp/connect/token"),\r\n Scopes = new Dictionary { ["api"] = "API access" }\r\n }\r\n }\r\n });\r\n});\n'})}),"\n",(0,i.jsx)(r.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(r.h3,{id:"accessing-the-current-user",children:"Accessing the current user"}),"\n",(0,i.jsxs)(r.p,{children:["Inject ",(0,i.jsx)(r.code,{children:"ICurrentUser"})," wherever you need identity information:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Users;\r\n\r\npublic class OrderCommandHandler\r\n{\r\n private readonly ICurrentUser _currentUser;\r\n private readonly IGraphRepository _orders;\r\n\r\n public OrderCommandHandler(ICurrentUser currentUser, IGraphRepository orders)\r\n {\r\n _currentUser = currentUser;\r\n _orders = orders;\r\n }\r\n\r\n public async Task Handle(PlaceOrderCommand command, CancellationToken ct)\r\n {\r\n if (!_currentUser.IsAuthenticated)\r\n throw new AuthorizationException("You must be signed in to place an order.");\r\n\r\n var order = new Order\r\n {\r\n CustomerId = _currentUser.Id!.Value,\r\n CustomerName = _currentUser.FindClaimValue(ClaimTypes.GivenName) ?? "Unknown",\r\n Total = command.Total\r\n };\r\n\r\n await _orders.AddAsync(order, ct);\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"reading-roles",children:"Reading roles"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'string[] roles = _currentUser.Roles; // distinct role values from ClaimTypesConst.Role claims\r\n\r\nif (!_currentUser.Roles.Contains("Administrator"))\r\n throw new AuthorizationException("Only administrators can perform this action.", "INSUFFICIENT_ROLE");\n'})}),"\n",(0,i.jsx)(r.h3,{id:"reading-arbitrary-claims",children:"Reading arbitrary claims"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'// Find the first matching claim.\r\nClaim? claim = _currentUser.FindClaim("custom:department");\r\n\r\n// Find all matching claims.\r\nClaim[] allDeptClaims = _currentUser.FindClaims("custom:department");\r\n\r\n// Find a claim value as a string.\r\nstring? department = _currentUser.FindClaimValue("custom:department");\r\n\r\n// Find a claim value converted to a specific struct type.\r\nint employeeNumber = _currentUser.FindClaimValue("custom:employee_number");\n'})}),"\n",(0,i.jsx)(r.h3,{id:"accessing-the-current-client",children:"Accessing the current client"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Clients;\r\n\r\npublic class ApiAuditMiddleware\r\n{\r\n private readonly ICurrentClient _currentClient;\r\n\r\n public ApiAuditMiddleware(ICurrentClient currentClient)\r\n {\r\n _currentClient = currentClient;\r\n }\r\n\r\n public void LogRequest(string path)\r\n {\r\n if (_currentClient.IsAuthenticated)\r\n {\r\n Console.WriteLine($"Client {_currentClient.Id} called {path}");\r\n }\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"temporarily-replacing-the-principal",children:"Temporarily replacing the principal"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor.Change"})," replaces the current principal for the lifetime of the returned ",(0,i.jsx)(r.code,{children:"IDisposable"}),". This is useful in background jobs and integration tests:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Claims;\r\nusing System.Security.Claims;\r\n\r\npublic async Task RunAsServiceAccountAsync(\r\n ICurrentPrincipalAccessor principalAccessor,\r\n Func work)\r\n{\r\n var identity = new ClaimsIdentity(new[]\r\n {\r\n new Claim(ClaimTypesConst.UserId, Guid.NewGuid().ToString()),\r\n new Claim(ClaimTypesConst.Role, "ServiceAccount")\r\n }, "ServiceAccount");\r\n\r\n using (principalAccessor.Change(new ClaimsPrincipal(identity)))\r\n {\r\n await work();\r\n // Previous principal is restored when the using block exits.\r\n }\r\n}\n'})}),"\n",(0,i.jsxs)(r.p,{children:["The extension overloads let you pass a single ",(0,i.jsx)(r.code,{children:"Claim"}),", a collection of claims, or a ",(0,i.jsx)(r.code,{children:"ClaimsIdentity"})," directly:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using (principalAccessor.Change(new Claim(ClaimTypesConst.Role, "Tester")))\r\n{\r\n // ...\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"signaling-authorization-failures",children:"Signaling authorization failures"}),"\n",(0,i.jsxs)(r.p,{children:["Throw ",(0,i.jsx)(r.code,{children:"AuthorizationException"})," from application-layer code when a rule is violated. Global exception handlers or middleware can then translate it to an appropriate HTTP 403 response:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Authorization;\r\n\r\npublic void EnsureCanEdit(Document document)\r\n{\r\n if (document.OwnerId != _currentUser.Id)\r\n {\r\n throw new AuthorizationException(\r\n message: "You do not have permission to edit this document.",\r\n code: "DOCUMENT_ACCESS_DENIED")\r\n .WithData("DocumentId", document.Id)\r\n .WithData("UserId", _currentUser.Id);\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"claims-identity-helpers",children:"Claims identity helpers"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"ClaimsIdentityExtensions"})," provides fluent extension methods for managing claims:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security;\r\n\r\n// Add a claim only if one with the same type does not already exist.\r\nidentity.AddIfNotContains(new Claim("custom:role", "Editor"));\r\n\r\n// Remove all claims of the same type and set a new value.\r\nidentity.AddOrReplace(new Claim("custom:role", "Administrator"));\r\n\r\n// Add an identity to a principal only if the same authentication type is not already present.\r\nprincipal.AddIdentityIfNotContains(new ClaimsIdentity(claims, "Cookie"));\n'})}),"\n",(0,i.jsxs)(r.p,{children:["Extract well-known values directly from a ",(0,i.jsx)(r.code,{children:"ClaimsPrincipal"}),":"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"Guid? userId = principal.FindUserId(); // parses ClaimTypesConst.UserId as Guid\r\nstring? tenant = principal.FindTenantId(); // reads ClaimTypesConst.TenantId\r\nstring? client = principal.FindClientId(); // reads ClaimTypesConst.ClientId\n"})}),"\n",(0,i.jsx)(r.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(r.table,{children:[(0,i.jsx)(r.thead,{children:(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.th,{children:"Type"}),(0,i.jsx)(r.th,{children:"Package"}),(0,i.jsx)(r.th,{children:"Description"})]})}),(0,i.jsxs)(r.tbody,{children:[(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Provides the current ",(0,i.jsx)(r.code,{children:"ClaimsPrincipal"}),"; supports scoped override via ",(0,i.jsx)(r.code,{children:"Change()"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentPrincipalAccessorBase"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Abstract base implementing ",(0,i.jsx)(r.code,{children:"Change()"})," with ",(0,i.jsx)(r.code,{children:"AsyncLocal"})," storage"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ThreadCurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Default implementation; reads ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentPrincipalAccessorExtensions"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:[(0,i.jsx)(r.code,{children:"Change(Claim)"}),", ",(0,i.jsx)(r.code,{children:"Change(IEnumerable)"}),", ",(0,i.jsx)(r.code,{children:"Change(ClaimsIdentity)"})," overloads"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentUser"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Exposes ",(0,i.jsx)(r.code,{children:"Id"}),", ",(0,i.jsx)(r.code,{children:"IsAuthenticated"}),", ",(0,i.jsx)(r.code,{children:"Roles"}),", ",(0,i.jsx)(r.code,{children:"TenantId"}),", ",(0,i.jsx)(r.code,{children:"FindClaim"}),", ",(0,i.jsx)(r.code,{children:"FindClaims"}),", ",(0,i.jsx)(r.code,{children:"GetAllClaims"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentUser"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Default ",(0,i.jsx)(r.code,{children:"ICurrentUser"})," implementation backed by ",(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentUserExtensions"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:[(0,i.jsx)(r.code,{children:"FindClaimValue(string)"}),", ",(0,i.jsx)(r.code,{children:"FindClaimValue(string)"}),", ",(0,i.jsx)(r.code,{children:"GetId()"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentClient"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Exposes ",(0,i.jsx)(r.code,{children:"Id"})," and ",(0,i.jsx)(r.code,{children:"IsAuthenticated"})," for OAuth client identities"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentClient"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Default ",(0,i.jsx)(r.code,{children:"ICurrentClient"})," backed by ",(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ClaimTypesConst"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Configurable claim type URIs: ",(0,i.jsx)(r.code,{children:"UserName"}),", ",(0,i.jsx)(r.code,{children:"Name"}),", ",(0,i.jsx)(r.code,{children:"SurName"}),", ",(0,i.jsx)(r.code,{children:"UserId"}),", ",(0,i.jsx)(r.code,{children:"Role"}),", ",(0,i.jsx)(r.code,{children:"Email"}),", ",(0,i.jsx)(r.code,{children:"TenantId"}),", ",(0,i.jsx)(r.code,{children:"ClientId"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ClaimsIdentityExtensions"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:[(0,i.jsx)(r.code,{children:"FindUserId"}),", ",(0,i.jsx)(r.code,{children:"FindTenantId"}),", ",(0,i.jsx)(r.code,{children:"FindClientId"}),", ",(0,i.jsx)(r.code,{children:"AddIfNotContains"}),", ",(0,i.jsx)(r.code,{children:"AddOrReplace"}),", ",(0,i.jsx)(r.code,{children:"AddIdentityIfNotContains"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"AuthorizationException"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Application-layer exception for access-denied scenarios; carries ",(0,i.jsx)(r.code,{children:"Code"}),", ",(0,i.jsx)(r.code,{children:"LogLevel"}),", and fluent ",(0,i.jsx)(r.code,{children:"WithData()"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"AuthorizeCheckOperationFilter"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Authorization.Web"})}),(0,i.jsxs)(r.td,{children:["Swashbuckle filter: adds 401/403 responses and OAuth2 security requirement to ",(0,i.jsx)(r.code,{children:"[Authorize]"}),"-decorated operations"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"AuthorizationHeaderParameterOperationFilter"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Authorization.Web"})}),(0,i.jsxs)(r.td,{children:["Swashbuckle filter: adds a required ",(0,i.jsx)(r.code,{children:"Authorization"})," header parameter to operations protected by ",(0,i.jsx)(r.code,{children:"AuthorizeFilter"})]})]})]})]})]})}function u(e={}){const{wrapper:r}={...(0,s.R)(),...e.components};return r?(0,i.jsx)(r,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},75783(e,r,n){n.d(r,{A:()=>l});var i=n(30758);const s="container_xjrG",t="label_Y4p8",c="commandRow_FY5I",d="command_m7Qs",a="copyButton_u1GK";var o=n(86070);function l({packageName:e,version:r}){const[n,l]=(0,i.useState)(!1),h=r?`dotnet add package ${e} --version ${r}`:`dotnet add package ${e}`;return(0,o.jsxs)("div",{className:s,children:[(0,o.jsx)("div",{className:t,children:"NuGet Package"}),(0,o.jsxs)("div",{className:c,children:[(0,o.jsx)("code",{className:d,children:h}),(0,o.jsx)("button",{className:a,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:n?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,r,n){n.d(r,{R:()=>c,x:()=>d});var i=n(30758);const s={},t=i.createContext(s);function c(e){const r=i.useContext(t);return i.useMemo(function(){return"function"==typeof e?e(r):{...r,...e}},[r,e])}function d(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:c(e.components),i.createElement(t.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/7d8ddf48.613f0a94.js b/website/build/assets/js/7d8ddf48.613f0a94.js new file mode 100644 index 00000000..cc6d58cf --- /dev/null +++ b/website/build/assets/js/7d8ddf48.613f0a94.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[1880],{91543(e,n,i){i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>l,default:()=>u,frontMatter:()=>t,metadata:()=>d,toc:()=>c});var s=i(86070),o=i(81753),r=i(75783);const t={title:"System.Text.Json",sidebar_position:3,description:"RCommon.SystemTextJson wraps System.Text.Json behind IJsonSerializer with full JsonSerializerOptions access and built-in integer and byte enum converters."},l="System.Text.Json",d={id:"serialization/system-text-json",title:"System.Text.Json",description:"RCommon.SystemTextJson wraps System.Text.Json behind IJsonSerializer with full JsonSerializerOptions access and built-in integer and byte enum converters.",source:"@site/docs/serialization/system-text-json.mdx",sourceDirName:"serialization",slug:"/serialization/system-text-json",permalink:"/docs/next/serialization/system-text-json",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/serialization/system-text-json.mdx",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"System.Text.Json",sidebar_position:3,description:"RCommon.SystemTextJson wraps System.Text.Json behind IJsonSerializer with full JsonSerializerOptions access and built-in integer and byte enum converters."},sidebar:"docsSidebar",previous:{title:"Newtonsoft.Json",permalink:"/docs/next/serialization/newtonsoft"},next:{title:"Validation",permalink:"/docs/next/category/validation"}},a={},c=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Default setup",id:"default-setup",level:3},{value:"Configuring global serialize options",id:"configuring-global-serialize-options",level:3},{value:"Configuring JsonSerializerOptions directly",id:"configuring-jsonserializeroptions-directly",level:3},{value:"Using enum converters",id:"using-enum-converters",level:3},{value:"Modular composition",id:"modular-composition",level:3},{value:"Usage",id:"usage",level:2},{value:"Per-call options",id:"per-call-options",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"TextJsonBuilder",id:"textjsonbuilder",level:3},{value:"ITextJsonBuilder extension methods",id:"itextjsonbuilder-extension-methods",level:3},{value:"TextJsonSerializer",id:"textjsonserializer",level:3},{value:"Built-in converters",id:"built-in-converters",level:3}];function h(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,o.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"systemtextjson",children:"System.Text.Json"}),"\n",(0,s.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"RCommon.SystemTextJson"})," wraps the built-in ",(0,s.jsx)(n.code,{children:"System.Text.Json"})," library behind the ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," interface. Because ",(0,s.jsx)(n.code,{children:"System.Text.Json"})," ships with the .NET runtime, this provider adds no external dependencies beyond the NuGet package itself, making it a good default for greenfield applications targeting .NET 6 or later."]}),"\n",(0,s.jsxs)(n.p,{children:["The underlying ",(0,s.jsx)(n.code,{children:"JsonSerializerOptions"})," instance is registered via the options pattern so you can configure every aspect of the serializer that ",(0,s.jsx)(n.code,{children:"JsonSerializerOptions"})," exposes, including custom converters."]}),"\n",(0,s.jsx)(n.p,{children:"The package also includes two ready-to-use converters:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"JsonIntEnumConverter"})," \u2014 serializes enums as integers."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"JsonByteEnumConverter"})," \u2014 serializes enums as bytes."]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,s.jsx)(r.A,{packageName:"RCommon.SystemTextJson"}),"\n",(0,s.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,s.jsx)(n.h3,{id:"default-setup",children:"Default setup"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\nusing RCommon.SystemTextJson;\n\nbuilder.Services.AddRCommon()\n .WithJsonSerialization();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["This registers ",(0,s.jsx)(n.code,{children:"TextJsonSerializer"})," as the ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," implementation with default ",(0,s.jsx)(n.code,{children:"JsonSerializeOptions"})," (camelCase enabled, indentation disabled)."]}),"\n",(0,s.jsx)(n.h3,{id:"configuring-global-serialize-options",children:"Configuring global serialize options"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithJsonSerialization(opts =>\n {\n opts.CamelCase = true;\n opts.Indented = true;\n });\n"})}),"\n",(0,s.jsx)(n.h3,{id:"configuring-jsonserializeroptions-directly",children:"Configuring JsonSerializerOptions directly"}),"\n",(0,s.jsxs)(n.p,{children:["Use the ",(0,s.jsx)(n.code,{children:"Configure"})," extension on ",(0,s.jsx)(n.code,{children:"ITextJsonBuilder"})," to access the underlying ",(0,s.jsx)(n.code,{children:"JsonSerializerOptions"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"using System.Text.Json;\nusing RCommon;\nusing RCommon.SystemTextJson;\n\nbuilder.Services.AddRCommon()\n .WithJsonSerialization(b =>\n {\n b.Configure(options =>\n {\n options.DefaultIgnoreCondition =\n System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;\n options.Converters.Add(new JsonStringEnumConverter());\n });\n });\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Combining global serialize options with custom ",(0,s.jsx)(n.code,{children:"JsonSerializerOptions"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithJsonSerialization(\n serializeOptions: opts =>\n {\n opts.CamelCase = true;\n opts.Indented = false;\n },\n deSerializeOptions: _ => { },\n actions: b =>\n {\n b.Configure(options =>\n {\n options.DefaultIgnoreCondition =\n System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;\n });\n });\n"})}),"\n",(0,s.jsx)(n.h3,{id:"using-enum-converters",children:"Using enum converters"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"using RCommon.SystemTextJson;\n\nbuilder.Services.AddRCommon()\n .WithJsonSerialization(b =>\n {\n b.Configure(options =>\n {\n // Serialize enums as integer values.\n options.Converters.Add(new JsonIntEnumConverter());\n });\n });\n"})}),"\n",(0,s.jsx)(n.h3,{id:"modular-composition",children:"Modular composition"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"WithJsonSerialization()"})," is a singleton-style verb across all six overloads (parameterless, ",(0,s.jsx)(n.code,{children:"serializeOptions"}),", ",(0,s.jsx)(n.code,{children:"serializeOptions + deSerializeOptions"}),", ",(0,s.jsx)(n.code,{children:"actions"}),", ",(0,s.jsx)(n.code,{children:"serializeOptions + actions"}),", and ",(0,s.jsx)(n.code,{children:"serializeOptions + deSerializeOptions + actions"}),"). Only one ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," implementation makes sense per process, so the verb enforces the choice at startup."]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Re-registration shape"}),(0,s.jsx)(n.th,{children:"Behaviour"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"WithJsonSerialization()"})," called from two modules"]}),(0,s.jsx)(n.td,{children:"Idempotent no-op \u2014 the second registration is skipped, but the configuration delegate still runs so per-call option values follow last-write-wins."})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.td,{children:["One module calls ",(0,s.jsx)(n.code,{children:"WithJsonSerialization()"})," and another calls ",(0,s.jsx)(n.code,{children:"WithJsonSerialization()"})]}),(0,s.jsxs)(n.td,{children:["Throws ",(0,s.jsx)(n.code,{children:"RCommonBuilderException"})," naming both builder types. Pick exactly one JSON serializer."]})]})]})]}),"\n",(0,s.jsxs)(n.p,{children:["See ",(0,s.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]}),"\n",(0,s.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,s.jsxs)(n.p,{children:["Inject ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," and call ",(0,s.jsx)(n.code,{children:"Serialize"})," or ",(0,s.jsx)(n.code,{children:"Deserialize"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public class EventPublisher\n{\n private readonly IJsonSerializer _serializer;\n\n public EventPublisher(IJsonSerializer serializer)\n {\n _serializer = serializer;\n }\n\n public string ToJson(DomainEvent @event)\n {\n return _serializer.Serialize(@event);\n }\n\n public DomainEvent? FromJson(string json)\n {\n return _serializer.Deserialize(json);\n }\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"per-call-options",children:"Per-call options"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"// Produce indented output for diagnostic logging.\nstring pretty = _serializer.Serialize(\n obj: payload,\n options: new JsonSerializeOptions { Indented = true, CamelCase = true });\n"})}),"\n",(0,s.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,s.jsx)(n.h3,{id:"textjsonbuilder",children:(0,s.jsx)(n.code,{children:"TextJsonBuilder"})}),"\n",(0,s.jsxs)(n.p,{children:["Implements ",(0,s.jsx)(n.code,{children:"IJsonBuilder"})," and ",(0,s.jsx)(n.code,{children:"ITextJsonBuilder"}),". Registered automatically by ",(0,s.jsx)(n.code,{children:"WithJsonSerialization"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["On construction it registers ",(0,s.jsx)(n.code,{children:"TextJsonSerializer"})," as ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," with a transient lifetime."]}),"\n",(0,s.jsxs)(n.h3,{id:"itextjsonbuilder-extension-methods",children:[(0,s.jsx)(n.code,{children:"ITextJsonBuilder"})," extension methods"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Method"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsx)(n.tbody,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"Configure(Action)"})}),(0,s.jsxs)(n.td,{children:["Configures the underlying ",(0,s.jsx)(n.code,{children:"JsonSerializerOptions"})," used by ",(0,s.jsx)(n.code,{children:"TextJsonSerializer"}),". Returns ",(0,s.jsx)(n.code,{children:"ITextJsonBuilder"})," for chaining."]})]})})]}),"\n",(0,s.jsx)(n.h3,{id:"textjsonserializer",children:(0,s.jsx)(n.code,{children:"TextJsonSerializer"})}),"\n",(0,s.jsxs)(n.p,{children:["Registered as ",(0,s.jsx)(n.code,{children:"IJsonSerializer"}),". Accepts ",(0,s.jsx)(n.code,{children:"IOptions"})," via constructor injection."]}),"\n",(0,s.jsxs)(n.p,{children:["Implements all four ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," methods. Per-call ",(0,s.jsx)(n.code,{children:"JsonSerializeOptions"})," or ",(0,s.jsx)(n.code,{children:"JsonDeserializeOptions"})," mutate the shared options instance for that call: ",(0,s.jsx)(n.code,{children:"CamelCase = true"})," sets ",(0,s.jsx)(n.code,{children:"PropertyNamingPolicy = JsonNamingPolicy.CamelCase"}),"; ",(0,s.jsx)(n.code,{children:"Indented = true"})," sets ",(0,s.jsx)(n.code,{children:"WriteIndented = true"}),"."]}),"\n",(0,s.jsx)(n.h3,{id:"built-in-converters",children:"Built-in converters"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"JsonIntEnumConverter"})}),(0,s.jsxs)(n.td,{children:["Serializes and deserializes enum values as ",(0,s.jsx)(n.code,{children:"int"}),"."]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"JsonByteEnumConverter"})}),(0,s.jsxs)(n.td,{children:["Serializes and deserializes enum values as ",(0,s.jsx)(n.code,{children:"byte"}),"."]})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}},75783(e,n,i){i.d(n,{A:()=>c});var s=i(30758);const o="container_xjrG",r="label_Y4p8",t="commandRow_FY5I",l="command_m7Qs",d="copyButton_u1GK";var a=i(86070);function c({packageName:e,version:n}){const[i,c]=(0,s.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,a.jsxs)("div",{className:o,children:[(0,a.jsx)("div",{className:r,children:"NuGet Package"}),(0,a.jsxs)("div",{className:t,children:[(0,a.jsx)("code",{className:l,children:h}),(0,a.jsx)("button",{className:d,onClick:()=>{navigator.clipboard.writeText(h),c(!0),setTimeout(()=>c(!1),2e3)},title:"Copy to clipboard",children:i?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,i){i.d(n,{R:()=>t,x:()=>l});var s=i(30758);const o={},r=s.createContext(o);function t(e){const n=s.useContext(r);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:t(e.components),s.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/7d8ddf48.b39b3c4e.js b/website/build/assets/js/7d8ddf48.b39b3c4e.js deleted file mode 100644 index 714211f8..00000000 --- a/website/build/assets/js/7d8ddf48.b39b3c4e.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[1880],{91543(e,n,i){i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>l,default:()=>u,frontMatter:()=>o,metadata:()=>d,toc:()=>c});var s=i(86070),r=i(81753),t=i(75783);const o={title:"System.Text.Json",sidebar_position:3,description:"RCommon.SystemTextJson wraps System.Text.Json behind IJsonSerializer with full JsonSerializerOptions access and built-in integer and byte enum converters."},l="System.Text.Json",d={id:"serialization/system-text-json",title:"System.Text.Json",description:"RCommon.SystemTextJson wraps System.Text.Json behind IJsonSerializer with full JsonSerializerOptions access and built-in integer and byte enum converters.",source:"@site/docs/serialization/system-text-json.mdx",sourceDirName:"serialization",slug:"/serialization/system-text-json",permalink:"/docs/next/serialization/system-text-json",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/serialization/system-text-json.mdx",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"System.Text.Json",sidebar_position:3,description:"RCommon.SystemTextJson wraps System.Text.Json behind IJsonSerializer with full JsonSerializerOptions access and built-in integer and byte enum converters."},sidebar:"docsSidebar",previous:{title:"Newtonsoft.Json",permalink:"/docs/next/serialization/newtonsoft"},next:{title:"Validation",permalink:"/docs/next/category/validation"}},a={},c=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Default setup",id:"default-setup",level:3},{value:"Configuring global serialize options",id:"configuring-global-serialize-options",level:3},{value:"Configuring JsonSerializerOptions directly",id:"configuring-jsonserializeroptions-directly",level:3},{value:"Using enum converters",id:"using-enum-converters",level:3},{value:"Usage",id:"usage",level:2},{value:"Per-call options",id:"per-call-options",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"TextJsonBuilder",id:"textjsonbuilder",level:3},{value:"ITextJsonBuilder extension methods",id:"itextjsonbuilder-extension-methods",level:3},{value:"TextJsonSerializer",id:"textjsonserializer",level:3},{value:"Built-in converters",id:"built-in-converters",level:3}];function h(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"systemtextjson",children:"System.Text.Json"}),"\n",(0,s.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"RCommon.SystemTextJson"})," wraps the built-in ",(0,s.jsx)(n.code,{children:"System.Text.Json"})," library behind the ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," interface. Because ",(0,s.jsx)(n.code,{children:"System.Text.Json"})," ships with the .NET runtime, this provider adds no external dependencies beyond the NuGet package itself, making it a good default for greenfield applications targeting .NET 6 or later."]}),"\n",(0,s.jsxs)(n.p,{children:["The underlying ",(0,s.jsx)(n.code,{children:"JsonSerializerOptions"})," instance is registered via the options pattern so you can configure every aspect of the serializer that ",(0,s.jsx)(n.code,{children:"JsonSerializerOptions"})," exposes, including custom converters."]}),"\n",(0,s.jsx)(n.p,{children:"The package also includes two ready-to-use converters:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"JsonIntEnumConverter"})," \u2014 serializes enums as integers."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"JsonByteEnumConverter"})," \u2014 serializes enums as bytes."]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,s.jsx)(t.A,{packageName:"RCommon.SystemTextJson"}),"\n",(0,s.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,s.jsx)(n.h3,{id:"default-setup",children:"Default setup"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\r\nusing RCommon.SystemTextJson;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithJsonSerialization();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["This registers ",(0,s.jsx)(n.code,{children:"TextJsonSerializer"})," as the ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," implementation with default ",(0,s.jsx)(n.code,{children:"JsonSerializeOptions"})," (camelCase enabled, indentation disabled)."]}),"\n",(0,s.jsx)(n.h3,{id:"configuring-global-serialize-options",children:"Configuring global serialize options"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithJsonSerialization(opts =>\r\n {\r\n opts.CamelCase = true;\r\n opts.Indented = true;\r\n });\n"})}),"\n",(0,s.jsx)(n.h3,{id:"configuring-jsonserializeroptions-directly",children:"Configuring JsonSerializerOptions directly"}),"\n",(0,s.jsxs)(n.p,{children:["Use the ",(0,s.jsx)(n.code,{children:"Configure"})," extension on ",(0,s.jsx)(n.code,{children:"ITextJsonBuilder"})," to access the underlying ",(0,s.jsx)(n.code,{children:"JsonSerializerOptions"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"using System.Text.Json;\r\nusing RCommon;\r\nusing RCommon.SystemTextJson;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithJsonSerialization(b =>\r\n {\r\n b.Configure(options =>\r\n {\r\n options.DefaultIgnoreCondition =\r\n System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;\r\n options.Converters.Add(new JsonStringEnumConverter());\r\n });\r\n });\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Combining global serialize options with custom ",(0,s.jsx)(n.code,{children:"JsonSerializerOptions"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithJsonSerialization(\r\n serializeOptions: opts =>\r\n {\r\n opts.CamelCase = true;\r\n opts.Indented = false;\r\n },\r\n deSerializeOptions: _ => { },\r\n actions: b =>\r\n {\r\n b.Configure(options =>\r\n {\r\n options.DefaultIgnoreCondition =\r\n System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;\r\n });\r\n });\n"})}),"\n",(0,s.jsx)(n.h3,{id:"using-enum-converters",children:"Using enum converters"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"using RCommon.SystemTextJson;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithJsonSerialization(b =>\r\n {\r\n b.Configure(options =>\r\n {\r\n // Serialize enums as integer values.\r\n options.Converters.Add(new JsonIntEnumConverter());\r\n });\r\n });\n"})}),"\n",(0,s.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,s.jsxs)(n.p,{children:["Inject ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," and call ",(0,s.jsx)(n.code,{children:"Serialize"})," or ",(0,s.jsx)(n.code,{children:"Deserialize"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public class EventPublisher\r\n{\r\n private readonly IJsonSerializer _serializer;\r\n\r\n public EventPublisher(IJsonSerializer serializer)\r\n {\r\n _serializer = serializer;\r\n }\r\n\r\n public string ToJson(DomainEvent @event)\r\n {\r\n return _serializer.Serialize(@event);\r\n }\r\n\r\n public DomainEvent? FromJson(string json)\r\n {\r\n return _serializer.Deserialize(json);\r\n }\r\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"per-call-options",children:"Per-call options"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"// Produce indented output for diagnostic logging.\r\nstring pretty = _serializer.Serialize(\r\n obj: payload,\r\n options: new JsonSerializeOptions { Indented = true, CamelCase = true });\n"})}),"\n",(0,s.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,s.jsx)(n.h3,{id:"textjsonbuilder",children:(0,s.jsx)(n.code,{children:"TextJsonBuilder"})}),"\n",(0,s.jsxs)(n.p,{children:["Implements ",(0,s.jsx)(n.code,{children:"IJsonBuilder"})," and ",(0,s.jsx)(n.code,{children:"ITextJsonBuilder"}),". Registered automatically by ",(0,s.jsx)(n.code,{children:"WithJsonSerialization"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["On construction it registers ",(0,s.jsx)(n.code,{children:"TextJsonSerializer"})," as ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," with a transient lifetime."]}),"\n",(0,s.jsxs)(n.h3,{id:"itextjsonbuilder-extension-methods",children:[(0,s.jsx)(n.code,{children:"ITextJsonBuilder"})," extension methods"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Method"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsx)(n.tbody,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"Configure(Action)"})}),(0,s.jsxs)(n.td,{children:["Configures the underlying ",(0,s.jsx)(n.code,{children:"JsonSerializerOptions"})," used by ",(0,s.jsx)(n.code,{children:"TextJsonSerializer"}),". Returns ",(0,s.jsx)(n.code,{children:"ITextJsonBuilder"})," for chaining."]})]})})]}),"\n",(0,s.jsx)(n.h3,{id:"textjsonserializer",children:(0,s.jsx)(n.code,{children:"TextJsonSerializer"})}),"\n",(0,s.jsxs)(n.p,{children:["Registered as ",(0,s.jsx)(n.code,{children:"IJsonSerializer"}),". Accepts ",(0,s.jsx)(n.code,{children:"IOptions"})," via constructor injection."]}),"\n",(0,s.jsxs)(n.p,{children:["Implements all four ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," methods. Per-call ",(0,s.jsx)(n.code,{children:"JsonSerializeOptions"})," or ",(0,s.jsx)(n.code,{children:"JsonDeserializeOptions"})," mutate the shared options instance for that call: ",(0,s.jsx)(n.code,{children:"CamelCase = true"})," sets ",(0,s.jsx)(n.code,{children:"PropertyNamingPolicy = JsonNamingPolicy.CamelCase"}),"; ",(0,s.jsx)(n.code,{children:"Indented = true"})," sets ",(0,s.jsx)(n.code,{children:"WriteIndented = true"}),"."]}),"\n",(0,s.jsx)(n.h3,{id:"built-in-converters",children:"Built-in converters"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"JsonIntEnumConverter"})}),(0,s.jsxs)(n.td,{children:["Serializes and deserializes enum values as ",(0,s.jsx)(n.code,{children:"int"}),"."]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"JsonByteEnumConverter"})}),(0,s.jsxs)(n.td,{children:["Serializes and deserializes enum values as ",(0,s.jsx)(n.code,{children:"byte"}),"."]})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}},75783(e,n,i){i.d(n,{A:()=>c});var s=i(30758);const r="container_xjrG",t="label_Y4p8",o="commandRow_FY5I",l="command_m7Qs",d="copyButton_u1GK";var a=i(86070);function c({packageName:e,version:n}){const[i,c]=(0,s.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,a.jsxs)("div",{className:r,children:[(0,a.jsx)("div",{className:t,children:"NuGet Package"}),(0,a.jsxs)("div",{className:o,children:[(0,a.jsx)("code",{className:l,children:h}),(0,a.jsx)("button",{className:d,onClick:()=>{navigator.clipboard.writeText(h),c(!0),setTimeout(()=>c(!1),2e3)},title:"Copy to clipboard",children:i?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,i){i.d(n,{R:()=>o,x:()=>l});var s=i(30758);const r={},t=s.createContext(r);function o(e){const n=s.useContext(t);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),s.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/8a13d96c.9eb6a0aa.js b/website/build/assets/js/8a13d96c.9eb6a0aa.js new file mode 100644 index 00000000..4192e81b --- /dev/null +++ b/website/build/assets/js/8a13d96c.9eb6a0aa.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[2042],{36471(e,n,t){t.r(n),t.d(n,{assets:()=>a,contentTitle:()=>d,default:()=>x,frontMatter:()=>s,metadata:()=>l,toc:()=>c});var r=t(86070),o=t(81753),i=t(75783);const s={title:"Overview",sidebar_position:1,description:"Provider-agnostic blob storage abstraction for .NET covering upload, download, metadata, presigned URLs, and named multi-store registration via IBlobStoreFactory."},d="Blob Storage Overview",l={id:"blob-storage/overview",title:"Overview",description:"Provider-agnostic blob storage abstraction for .NET covering upload, download, metadata, presigned URLs, and named multi-store registration via IBlobStoreFactory.",source:"@site/docs/blob-storage/overview.mdx",sourceDirName:"blob-storage",slug:"/blob-storage/overview",permalink:"/docs/next/blob-storage/overview",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/blob-storage/overview.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Overview",sidebar_position:1,description:"Provider-agnostic blob storage abstraction for .NET covering upload, download, metadata, presigned URLs, and named multi-store registration via IBlobStoreFactory."},sidebar:"docsSidebar",previous:{title:"Blob Storage",permalink:"/docs/next/category/blob-storage"},next:{title:"Azure Blob Storage",permalink:"/docs/next/blob-storage/azure"}},a={},c=[{value:"Overview",id:"overview",level:2},{value:"Multiple named stores",id:"multiple-named-stores",level:3},{value:"Core abstractions",id:"core-abstractions",level:3},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Usage",id:"usage",level:2},{value:"Resolving a store",id:"resolving-a-store",level:3},{value:"Uploading a blob",id:"uploading-a-blob",level:3},{value:"Downloading a blob",id:"downloading-a-blob",level:3},{value:"Listing blobs",id:"listing-blobs",level:3},{value:"Reading and updating metadata",id:"reading-and-updating-metadata",level:3},{value:"Generating a presigned download URL",id:"generating-a-presigned-download-url",level:3},{value:"Copying and moving blobs",id:"copying-and-moving-blobs",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"IBlobStorageService",id:"iblobstorageservice",level:3},{value:"IBlobStoreFactory",id:"iblobstorefactory",level:3},{value:"BlobUploadOptions",id:"blobuploadoptions",level:3},{value:"BlobItem",id:"blobitem",level:3},{value:"BlobProperties",id:"blobproperties",level:3}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,o.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"blob-storage-overview",children:"Blob Storage Overview"}),"\n",(0,r.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,r.jsxs)(n.p,{children:["RCommon provides a provider-agnostic abstraction for blob and object storage. The abstraction covers the full lifecycle of binary content: container management, blob upload and download, metadata, copy/move, and presigned URL generation. Your application code depends only on ",(0,r.jsx)(n.code,{children:"IBlobStorageService"})," \u2014 switching from Azure Blob Storage to Amazon S3 (or a local substitute for testing) is a configuration-only change."]}),"\n",(0,r.jsx)(n.h3,{id:"multiple-named-stores",children:"Multiple named stores"}),"\n",(0,r.jsxs)(n.p,{children:["Applications that need more than one storage backend register each store under a unique name. The ",(0,r.jsx)(n.code,{children:"IBlobStoreFactory"})," resolves the correct ",(0,r.jsx)(n.code,{children:"IBlobStorageService"})," at runtime based on that name. This is useful when, for example, user uploads go to one bucket while generated reports go to another."]}),"\n",(0,r.jsx)(n.h3,{id:"core-abstractions",children:"Core abstractions"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"IBlobStorageService"})," is the primary interface. It groups operations into four categories:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Container operations"})," \u2014 create, delete, check existence, list containers."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Blob CRUD"})," \u2014 upload, download, delete, check existence, list blobs (with optional prefix filter)."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Metadata"})," \u2014 read detailed blob properties, set arbitrary key/value metadata."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Transfer"})," \u2014 copy or move a blob between containers without downloading it locally."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Presigned URLs"})," \u2014 generate time-limited download or upload URLs that can be shared with external clients."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"IBlobStoreFactory"})," resolves a named ",(0,r.jsx)(n.code,{children:"IBlobStorageService"})," that was registered during startup."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"BlobUploadOptions"})," controls how a blob is stored during upload."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"BlobItem"})," represents a single entry returned from listing operations."]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"BlobProperties"})," carries the full metadata of an existing blob."]}),"\n",(0,r.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(n.p,{children:"Install the core abstractions package:"}),"\n",(0,r.jsx)(i.A,{packageName:"RCommon.Blobs"}),"\n",(0,r.jsx)(n.p,{children:"Then install the provider package that matches your storage backend:"}),"\n",(0,r.jsx)(i.A,{packageName:"RCommon.Azure.Blobs"}),"\n",(0,r.jsx)(n.p,{children:"or"}),"\n",(0,r.jsx)(i.A,{packageName:"RCommon.Amazon.S3Objects"}),"\n",(0,r.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsxs)(n.p,{children:["Register one or more blob stores inside ",(0,r.jsx)(n.code,{children:"AddRCommon()"}),". Each call to ",(0,r.jsx)(n.code,{children:"WithBlobStorage"})," registers a builder of type ",(0,r.jsx)(n.code,{children:"T"})," and makes the named stores it configures available through ",(0,r.jsx)(n.code,{children:"IBlobStoreFactory"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon;\nusing RCommon.Azure.Blobs;\n\nbuilder.Services.AddRCommon()\n .WithBlobStorage(azure =>\n {\n azure.AddBlobStore("uploads", opts =>\n {\n opts.ConnectionString = builder.Configuration\n .GetConnectionString("AzureStorage");\n });\n });\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Multiple providers can be registered in a single application by chaining additional ",(0,r.jsx)(n.code,{children:"WithBlobStorage"})," calls:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'builder.Services.AddRCommon()\n .WithBlobStorage(azure =>\n {\n azure.AddBlobStore("hot", opts =>\n opts.ConnectionString = azureConnectionString);\n })\n .WithBlobStorage(s3 =>\n {\n s3.AddBlobStore("archive", opts =>\n {\n opts.Region = "us-east-1";\n opts.AccessKeyId = awsKey;\n opts.SecretAccessKey = awsSecret;\n });\n });\n'})}),"\n",(0,r.jsx)(n.admonition,{title:"Modular composition",type:"tip",children:(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"WithBlobStorage"})," is a cache-aware sub-builder verb. When multiple modules call ",(0,r.jsx)(n.code,{children:"WithBlobStorage"})," (or any single concrete builder), the cached builder is reused and each module's ",(0,r.jsx)(n.code,{children:"AddBlobStore(name, ...)"})," calls accumulate on the same instance \u2014 store names from every module are visible through ",(0,r.jsx)(n.code,{children:"IBlobStoreFactory"}),". Different concrete builder types (e.g. one module using ",(0,r.jsx)(n.code,{children:"AzureBlobStorageBuilder"})," and another using ",(0,r.jsx)(n.code,{children:"AmazonS3ObjectsBuilder"}),") coexist side by side, exactly as the chained example above shows; calling the same builder type twice with the same store name is idempotent for that name's options. See ",(0,r.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,r.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,r.jsx)(n.h3,{id:"resolving-a-store",children:"Resolving a store"}),"\n",(0,r.jsxs)(n.p,{children:["Inject ",(0,r.jsx)(n.code,{children:"IBlobStoreFactory"})," and call ",(0,r.jsx)(n.code,{children:"Resolve"})," with the name you used at registration:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'public class DocumentService\n{\n private readonly IBlobStorageService _storage;\n\n public DocumentService(IBlobStoreFactory factory)\n {\n _storage = factory.Resolve("uploads");\n }\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"uploading-a-blob",children:"Uploading a blob"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'await using var stream = File.OpenRead("/tmp/report.pdf");\n\nawait _storage.UploadAsync(\n containerName: "documents",\n blobName: "reports/q1-2025.pdf",\n content: stream,\n options: new BlobUploadOptions\n {\n ContentType = "application/pdf",\n Overwrite = true,\n Metadata = new Dictionary\n {\n ["author"] = "finance-bot"\n }\n });\n'})}),"\n",(0,r.jsx)(n.h3,{id:"downloading-a-blob",children:"Downloading a blob"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'Stream content = await _storage.DownloadAsync("documents", "reports/q1-2025.pdf");\n'})}),"\n",(0,r.jsx)(n.h3,{id:"listing-blobs",children:"Listing blobs"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'IEnumerable items = await _storage.ListBlobsAsync(\n containerName: "documents",\n prefix: "reports/");\n\nforeach (BlobItem item in items)\n{\n Console.WriteLine($"{item.Name} {item.Size} bytes {item.LastModified}");\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"reading-and-updating-metadata",children:"Reading and updating metadata"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'BlobProperties props = await _storage.GetPropertiesAsync("documents", "reports/q1-2025.pdf");\nConsole.WriteLine(props.ContentType);\nConsole.WriteLine(props.ContentLength);\n\nawait _storage.SetMetadataAsync(\n "documents",\n "reports/q1-2025.pdf",\n new Dictionary { ["reviewed"] = "true" });\n'})}),"\n",(0,r.jsx)(n.h3,{id:"generating-a-presigned-download-url",children:"Generating a presigned download URL"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'Uri url = await _storage.GetPresignedDownloadUrlAsync(\n containerName: "documents",\n blobName: "reports/q1-2025.pdf",\n expiry: TimeSpan.FromHours(1));\n\n// Share `url` with a client \u2014 it expires after one hour.\n'})}),"\n",(0,r.jsx)(n.h3,{id:"copying-and-moving-blobs",children:"Copying and moving blobs"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'// Copy without removing the source.\nawait _storage.CopyAsync("documents", "reports/q1-2025.pdf",\n "archive", "2025/q1-final.pdf");\n\n// Move (copy then delete source).\nawait _storage.MoveAsync("staging", "upload-001.pdf",\n "documents", "final-001.pdf");\n'})}),"\n",(0,r.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,r.jsx)(n.h3,{id:"iblobstorageservice",children:(0,r.jsx)(n.code,{children:"IBlobStorageService"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Method"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"CreateContainerAsync(name)"})}),(0,r.jsx)(n.td,{children:"Creates a new container or bucket."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"DeleteContainerAsync(name)"})}),(0,r.jsx)(n.td,{children:"Deletes a container and all its contents."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ContainerExistsAsync(name)"})}),(0,r.jsxs)(n.td,{children:["Returns ",(0,r.jsx)(n.code,{children:"true"})," if the named container exists."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ListContainersAsync()"})}),(0,r.jsx)(n.td,{children:"Returns the names of all containers in the store."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"UploadAsync(container, blob, stream, options?)"})}),(0,r.jsx)(n.td,{children:"Uploads content to a blob."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"DownloadAsync(container, blob)"})}),(0,r.jsx)(n.td,{children:"Returns a readable stream for the blob content."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"DeleteAsync(container, blob)"})}),(0,r.jsx)(n.td,{children:"Permanently removes a blob."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ExistsAsync(container, blob)"})}),(0,r.jsxs)(n.td,{children:["Returns ",(0,r.jsx)(n.code,{children:"true"})," if the blob exists."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ListBlobsAsync(container, prefix?)"})}),(0,r.jsx)(n.td,{children:"Lists blobs, optionally filtered by prefix."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"GetPropertiesAsync(container, blob)"})}),(0,r.jsx)(n.td,{children:"Returns content type, size, ETag, and metadata."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SetMetadataAsync(container, blob, metadata)"})}),(0,r.jsx)(n.td,{children:"Replaces the blob's custom metadata."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"CopyAsync(srcContainer, srcBlob, dstContainer, dstBlob)"})}),(0,r.jsx)(n.td,{children:"Copies a blob without downloading it."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"MoveAsync(srcContainer, srcBlob, dstContainer, dstBlob)"})}),(0,r.jsx)(n.td,{children:"Copies then deletes the source blob."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"GetPresignedDownloadUrlAsync(container, blob, expiry)"})}),(0,r.jsx)(n.td,{children:"Generates a time-limited download URL."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"GetPresignedUploadUrlAsync(container, blob, expiry)"})}),(0,r.jsx)(n.td,{children:"Generates a time-limited upload URL."})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"iblobstorefactory",children:(0,r.jsx)(n.code,{children:"IBlobStoreFactory"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Method"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsx)(n.tbody,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"Resolve(name)"})}),(0,r.jsxs)(n.td,{children:["Returns the ",(0,r.jsx)(n.code,{children:"IBlobStorageService"})," registered under the given name. Throws ",(0,r.jsx)(n.code,{children:"BlobStoreNotFoundException"})," if the name is not registered."]})]})})]}),"\n",(0,r.jsx)(n.h3,{id:"blobuploadoptions",children:(0,r.jsx)(n.code,{children:"BlobUploadOptions"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Property"}),(0,r.jsx)(n.th,{children:"Default"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ContentType"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"null"})}),(0,r.jsx)(n.td,{children:"MIME type stored with the blob."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"Metadata"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"null"})}),(0,r.jsx)(n.td,{children:"Key/value pairs attached to the blob."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"Overwrite"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"true"})}),(0,r.jsx)(n.td,{children:"Whether to replace an existing blob with the same name."})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"blobitem",children:(0,r.jsx)(n.code,{children:"BlobItem"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Property"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"Name"})}),(0,r.jsx)(n.td,{children:"Blob name as returned by the provider."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"Size"})}),(0,r.jsx)(n.td,{children:"Size in bytes, if available."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ContentType"})}),(0,r.jsx)(n.td,{children:"MIME type, if available."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"LastModified"})}),(0,r.jsx)(n.td,{children:"Timestamp of last modification, if available."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"Metadata"})}),(0,r.jsx)(n.td,{children:"Custom key/value metadata attached to the blob."})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"blobproperties",children:(0,r.jsx)(n.code,{children:"BlobProperties"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Property"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ContentType"})}),(0,r.jsx)(n.td,{children:"MIME type of the blob."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ContentLength"})}),(0,r.jsx)(n.td,{children:"Size in bytes."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"LastModified"})}),(0,r.jsx)(n.td,{children:"Timestamp of last modification."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"ETag"})}),(0,r.jsx)(n.td,{children:"Entity tag for optimistic concurrency."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"Metadata"})}),(0,r.jsx)(n.td,{children:"Custom key/value metadata attached to the blob."})]})]})]})]})}function x(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},75783(e,n,t){t.d(n,{A:()=>c});var r=t(30758);const o="container_xjrG",i="label_Y4p8",s="commandRow_FY5I",d="command_m7Qs",l="copyButton_u1GK";var a=t(86070);function c({packageName:e,version:n}){const[t,c]=(0,r.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,a.jsxs)("div",{className:o,children:[(0,a.jsx)("div",{className:i,children:"NuGet Package"}),(0,a.jsxs)("div",{className:s,children:[(0,a.jsx)("code",{className:d,children:h}),(0,a.jsx)("button",{className:l,onClick:()=>{navigator.clipboard.writeText(h),c(!0),setTimeout(()=>c(!1),2e3)},title:"Copy to clipboard",children:t?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,t){t.d(n,{R:()=>s,x:()=>d});var r=t(30758);const o={},i=r.createContext(o);function s(e){const n=r.useContext(i);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:s(e.components),r.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/8a13d96c.e7d1c992.js b/website/build/assets/js/8a13d96c.e7d1c992.js deleted file mode 100644 index c5c63728..00000000 --- a/website/build/assets/js/8a13d96c.e7d1c992.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[2042],{36471(e,n,r){r.r(n),r.d(n,{assets:()=>a,contentTitle:()=>d,default:()=>x,frontMatter:()=>s,metadata:()=>l,toc:()=>c});var t=r(86070),o=r(81753),i=r(75783);const s={title:"Overview",sidebar_position:1,description:"Provider-agnostic blob storage abstraction for .NET covering upload, download, metadata, presigned URLs, and named multi-store registration via IBlobStoreFactory."},d="Blob Storage Overview",l={id:"blob-storage/overview",title:"Overview",description:"Provider-agnostic blob storage abstraction for .NET covering upload, download, metadata, presigned URLs, and named multi-store registration via IBlobStoreFactory.",source:"@site/docs/blob-storage/overview.mdx",sourceDirName:"blob-storage",slug:"/blob-storage/overview",permalink:"/docs/next/blob-storage/overview",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/blob-storage/overview.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Overview",sidebar_position:1,description:"Provider-agnostic blob storage abstraction for .NET covering upload, download, metadata, presigned URLs, and named multi-store registration via IBlobStoreFactory."},sidebar:"docsSidebar",previous:{title:"Blob Storage",permalink:"/docs/next/category/blob-storage"},next:{title:"Azure Blob Storage",permalink:"/docs/next/blob-storage/azure"}},a={},c=[{value:"Overview",id:"overview",level:2},{value:"Multiple named stores",id:"multiple-named-stores",level:3},{value:"Core abstractions",id:"core-abstractions",level:3},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Usage",id:"usage",level:2},{value:"Resolving a store",id:"resolving-a-store",level:3},{value:"Uploading a blob",id:"uploading-a-blob",level:3},{value:"Downloading a blob",id:"downloading-a-blob",level:3},{value:"Listing blobs",id:"listing-blobs",level:3},{value:"Reading and updating metadata",id:"reading-and-updating-metadata",level:3},{value:"Generating a presigned download URL",id:"generating-a-presigned-download-url",level:3},{value:"Copying and moving blobs",id:"copying-and-moving-blobs",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"IBlobStorageService",id:"iblobstorageservice",level:3},{value:"IBlobStoreFactory",id:"iblobstorefactory",level:3},{value:"BlobUploadOptions",id:"blobuploadoptions",level:3},{value:"BlobItem",id:"blobitem",level:3},{value:"BlobProperties",id:"blobproperties",level:3}];function h(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,o.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"blob-storage-overview",children:"Blob Storage Overview"}),"\n",(0,t.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,t.jsxs)(n.p,{children:["RCommon provides a provider-agnostic abstraction for blob and object storage. The abstraction covers the full lifecycle of binary content: container management, blob upload and download, metadata, copy/move, and presigned URL generation. Your application code depends only on ",(0,t.jsx)(n.code,{children:"IBlobStorageService"})," \u2014 switching from Azure Blob Storage to Amazon S3 (or a local substitute for testing) is a configuration-only change."]}),"\n",(0,t.jsx)(n.h3,{id:"multiple-named-stores",children:"Multiple named stores"}),"\n",(0,t.jsxs)(n.p,{children:["Applications that need more than one storage backend register each store under a unique name. The ",(0,t.jsx)(n.code,{children:"IBlobStoreFactory"})," resolves the correct ",(0,t.jsx)(n.code,{children:"IBlobStorageService"})," at runtime based on that name. This is useful when, for example, user uploads go to one bucket while generated reports go to another."]}),"\n",(0,t.jsx)(n.h3,{id:"core-abstractions",children:"Core abstractions"}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"IBlobStorageService"})," is the primary interface. It groups operations into four categories:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Container operations"})," \u2014 create, delete, check existence, list containers."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Blob CRUD"})," \u2014 upload, download, delete, check existence, list blobs (with optional prefix filter)."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Metadata"})," \u2014 read detailed blob properties, set arbitrary key/value metadata."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Transfer"})," \u2014 copy or move a blob between containers without downloading it locally."]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.strong,{children:"Presigned URLs"})," \u2014 generate time-limited download or upload URLs that can be shared with external clients."]}),"\n"]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"IBlobStoreFactory"})," resolves a named ",(0,t.jsx)(n.code,{children:"IBlobStorageService"})," that was registered during startup."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"BlobUploadOptions"})," controls how a blob is stored during upload."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"BlobItem"})," represents a single entry returned from listing operations."]}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"BlobProperties"})," carries the full metadata of an existing blob."]}),"\n",(0,t.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,t.jsx)(n.p,{children:"Install the core abstractions package:"}),"\n",(0,t.jsx)(i.A,{packageName:"RCommon.Blobs"}),"\n",(0,t.jsx)(n.p,{children:"Then install the provider package that matches your storage backend:"}),"\n",(0,t.jsx)(i.A,{packageName:"RCommon.Azure.Blobs"}),"\n",(0,t.jsx)(n.p,{children:"or"}),"\n",(0,t.jsx)(i.A,{packageName:"RCommon.Amazon.S3Objects"}),"\n",(0,t.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsxs)(n.p,{children:["Register one or more blob stores inside ",(0,t.jsx)(n.code,{children:"AddRCommon()"}),". Each call to ",(0,t.jsx)(n.code,{children:"WithBlobStorage"})," registers a builder of type ",(0,t.jsx)(n.code,{children:"T"})," and makes the named stores it configures available through ",(0,t.jsx)(n.code,{children:"IBlobStoreFactory"}),"."]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'using RCommon;\r\nusing RCommon.Azure.Blobs;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithBlobStorage(azure =>\r\n {\r\n azure.AddBlobStore("uploads", opts =>\r\n {\r\n opts.ConnectionString = builder.Configuration\r\n .GetConnectionString("AzureStorage");\r\n });\r\n });\n'})}),"\n",(0,t.jsxs)(n.p,{children:["Multiple providers can be registered in a single application by chaining additional ",(0,t.jsx)(n.code,{children:"WithBlobStorage"})," calls:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'builder.Services.AddRCommon()\r\n .WithBlobStorage(azure =>\r\n {\r\n azure.AddBlobStore("hot", opts =>\r\n opts.ConnectionString = azureConnectionString);\r\n })\r\n .WithBlobStorage(s3 =>\r\n {\r\n s3.AddBlobStore("archive", opts =>\r\n {\r\n opts.Region = "us-east-1";\r\n opts.AccessKeyId = awsKey;\r\n opts.SecretAccessKey = awsSecret;\r\n });\r\n });\n'})}),"\n",(0,t.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,t.jsx)(n.h3,{id:"resolving-a-store",children:"Resolving a store"}),"\n",(0,t.jsxs)(n.p,{children:["Inject ",(0,t.jsx)(n.code,{children:"IBlobStoreFactory"})," and call ",(0,t.jsx)(n.code,{children:"Resolve"})," with the name you used at registration:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'public class DocumentService\r\n{\r\n private readonly IBlobStorageService _storage;\r\n\r\n public DocumentService(IBlobStoreFactory factory)\r\n {\r\n _storage = factory.Resolve("uploads");\r\n }\r\n}\n'})}),"\n",(0,t.jsx)(n.h3,{id:"uploading-a-blob",children:"Uploading a blob"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'await using var stream = File.OpenRead("/tmp/report.pdf");\r\n\r\nawait _storage.UploadAsync(\r\n containerName: "documents",\r\n blobName: "reports/q1-2025.pdf",\r\n content: stream,\r\n options: new BlobUploadOptions\r\n {\r\n ContentType = "application/pdf",\r\n Overwrite = true,\r\n Metadata = new Dictionary\r\n {\r\n ["author"] = "finance-bot"\r\n }\r\n });\n'})}),"\n",(0,t.jsx)(n.h3,{id:"downloading-a-blob",children:"Downloading a blob"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'Stream content = await _storage.DownloadAsync("documents", "reports/q1-2025.pdf");\n'})}),"\n",(0,t.jsx)(n.h3,{id:"listing-blobs",children:"Listing blobs"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'IEnumerable items = await _storage.ListBlobsAsync(\r\n containerName: "documents",\r\n prefix: "reports/");\r\n\r\nforeach (BlobItem item in items)\r\n{\r\n Console.WriteLine($"{item.Name} {item.Size} bytes {item.LastModified}");\r\n}\n'})}),"\n",(0,t.jsx)(n.h3,{id:"reading-and-updating-metadata",children:"Reading and updating metadata"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'BlobProperties props = await _storage.GetPropertiesAsync("documents", "reports/q1-2025.pdf");\r\nConsole.WriteLine(props.ContentType);\r\nConsole.WriteLine(props.ContentLength);\r\n\r\nawait _storage.SetMetadataAsync(\r\n "documents",\r\n "reports/q1-2025.pdf",\r\n new Dictionary { ["reviewed"] = "true" });\n'})}),"\n",(0,t.jsx)(n.h3,{id:"generating-a-presigned-download-url",children:"Generating a presigned download URL"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'Uri url = await _storage.GetPresignedDownloadUrlAsync(\r\n containerName: "documents",\r\n blobName: "reports/q1-2025.pdf",\r\n expiry: TimeSpan.FromHours(1));\r\n\r\n// Share `url` with a client \u2014 it expires after one hour.\n'})}),"\n",(0,t.jsx)(n.h3,{id:"copying-and-moving-blobs",children:"Copying and moving blobs"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:'// Copy without removing the source.\r\nawait _storage.CopyAsync("documents", "reports/q1-2025.pdf",\r\n "archive", "2025/q1-final.pdf");\r\n\r\n// Move (copy then delete source).\r\nawait _storage.MoveAsync("staging", "upload-001.pdf",\r\n "documents", "final-001.pdf");\n'})}),"\n",(0,t.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,t.jsx)(n.h3,{id:"iblobstorageservice",children:(0,t.jsx)(n.code,{children:"IBlobStorageService"})}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Method"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"CreateContainerAsync(name)"})}),(0,t.jsx)(n.td,{children:"Creates a new container or bucket."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"DeleteContainerAsync(name)"})}),(0,t.jsx)(n.td,{children:"Deletes a container and all its contents."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"ContainerExistsAsync(name)"})}),(0,t.jsxs)(n.td,{children:["Returns ",(0,t.jsx)(n.code,{children:"true"})," if the named container exists."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"ListContainersAsync()"})}),(0,t.jsx)(n.td,{children:"Returns the names of all containers in the store."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"UploadAsync(container, blob, stream, options?)"})}),(0,t.jsx)(n.td,{children:"Uploads content to a blob."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"DownloadAsync(container, blob)"})}),(0,t.jsx)(n.td,{children:"Returns a readable stream for the blob content."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"DeleteAsync(container, blob)"})}),(0,t.jsx)(n.td,{children:"Permanently removes a blob."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"ExistsAsync(container, blob)"})}),(0,t.jsxs)(n.td,{children:["Returns ",(0,t.jsx)(n.code,{children:"true"})," if the blob exists."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"ListBlobsAsync(container, prefix?)"})}),(0,t.jsx)(n.td,{children:"Lists blobs, optionally filtered by prefix."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"GetPropertiesAsync(container, blob)"})}),(0,t.jsx)(n.td,{children:"Returns content type, size, ETag, and metadata."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"SetMetadataAsync(container, blob, metadata)"})}),(0,t.jsx)(n.td,{children:"Replaces the blob's custom metadata."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"CopyAsync(srcContainer, srcBlob, dstContainer, dstBlob)"})}),(0,t.jsx)(n.td,{children:"Copies a blob without downloading it."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"MoveAsync(srcContainer, srcBlob, dstContainer, dstBlob)"})}),(0,t.jsx)(n.td,{children:"Copies then deletes the source blob."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"GetPresignedDownloadUrlAsync(container, blob, expiry)"})}),(0,t.jsx)(n.td,{children:"Generates a time-limited download URL."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"GetPresignedUploadUrlAsync(container, blob, expiry)"})}),(0,t.jsx)(n.td,{children:"Generates a time-limited upload URL."})]})]})]}),"\n",(0,t.jsx)(n.h3,{id:"iblobstorefactory",children:(0,t.jsx)(n.code,{children:"IBlobStoreFactory"})}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Method"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsx)(n.tbody,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Resolve(name)"})}),(0,t.jsxs)(n.td,{children:["Returns the ",(0,t.jsx)(n.code,{children:"IBlobStorageService"})," registered under the given name. Throws ",(0,t.jsx)(n.code,{children:"BlobStoreNotFoundException"})," if the name is not registered."]})]})})]}),"\n",(0,t.jsx)(n.h3,{id:"blobuploadoptions",children:(0,t.jsx)(n.code,{children:"BlobUploadOptions"})}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Property"}),(0,t.jsx)(n.th,{children:"Default"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"ContentType"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"null"})}),(0,t.jsx)(n.td,{children:"MIME type stored with the blob."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Metadata"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"null"})}),(0,t.jsx)(n.td,{children:"Key/value pairs attached to the blob."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Overwrite"})}),(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"true"})}),(0,t.jsx)(n.td,{children:"Whether to replace an existing blob with the same name."})]})]})]}),"\n",(0,t.jsx)(n.h3,{id:"blobitem",children:(0,t.jsx)(n.code,{children:"BlobItem"})}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Property"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Name"})}),(0,t.jsx)(n.td,{children:"Blob name as returned by the provider."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Size"})}),(0,t.jsx)(n.td,{children:"Size in bytes, if available."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"ContentType"})}),(0,t.jsx)(n.td,{children:"MIME type, if available."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"LastModified"})}),(0,t.jsx)(n.td,{children:"Timestamp of last modification, if available."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Metadata"})}),(0,t.jsx)(n.td,{children:"Custom key/value metadata attached to the blob."})]})]})]}),"\n",(0,t.jsx)(n.h3,{id:"blobproperties",children:(0,t.jsx)(n.code,{children:"BlobProperties"})}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Property"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"ContentType"})}),(0,t.jsx)(n.td,{children:"MIME type of the blob."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"ContentLength"})}),(0,t.jsx)(n.td,{children:"Size in bytes."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"LastModified"})}),(0,t.jsx)(n.td,{children:"Timestamp of last modification."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"ETag"})}),(0,t.jsx)(n.td,{children:"Entity tag for optimistic concurrency."})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Metadata"})}),(0,t.jsx)(n.td,{children:"Custom key/value metadata attached to the blob."})]})]})]})]})}function x(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(h,{...e})}):h(e)}},75783(e,n,r){r.d(n,{A:()=>c});var t=r(30758);const o="container_xjrG",i="label_Y4p8",s="commandRow_FY5I",d="command_m7Qs",l="copyButton_u1GK";var a=r(86070);function c({packageName:e,version:n}){const[r,c]=(0,t.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,a.jsxs)("div",{className:o,children:[(0,a.jsx)("div",{className:i,children:"NuGet Package"}),(0,a.jsxs)("div",{className:s,children:[(0,a.jsx)("code",{className:d,children:h}),(0,a.jsx)("button",{className:l,onClick:()=>{navigator.clipboard.writeText(h),c(!0),setTimeout(()=>c(!1),2e3)},title:"Copy to clipboard",children:r?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,r){r.d(n,{R:()=>s,x:()=>d});var t=r(30758);const o={},i=t.createContext(o);function s(e){const n=t.useContext(i);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:s(e.components),t.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/8b219ab3.1bd3a992.js b/website/build/assets/js/8b219ab3.1bd3a992.js new file mode 100644 index 00000000..0506896b --- /dev/null +++ b/website/build/assets/js/8b219ab3.1bd3a992.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[3709],{50631(e,n,r){r.r(n),r.d(n,{assets:()=>o,contentTitle:()=>d,default:()=>u,frontMatter:()=>t,metadata:()=>a,toc:()=>l});var s=r(86070),i=r(81753);r(85363),r(64996);const t={title:"Event-Driven Architecture",sidebar_position:3,description:"Implement event-driven .NET apps with RCommon using IEventProducer and ISubscriber across in-memory, MediatR, MassTransit, and Wolverine providers."},d="Event-Driven Architecture with RCommon",a={id:"architecture-guides/event-driven",title:"Event-Driven Architecture",description:"Implement event-driven .NET apps with RCommon using IEventProducer and ISubscriber across in-memory, MediatR, MassTransit, and Wolverine providers.",source:"@site/docs/architecture-guides/event-driven.mdx",sourceDirName:"architecture-guides",slug:"/architecture-guides/event-driven",permalink:"/docs/next/architecture-guides/event-driven",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/architecture-guides/event-driven.mdx",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"Event-Driven Architecture",sidebar_position:3,description:"Implement event-driven .NET apps with RCommon using IEventProducer and ISubscriber across in-memory, MediatR, MassTransit, and Wolverine providers."},sidebar:"docsSidebar",previous:{title:"Microservices",permalink:"/docs/next/architecture-guides/microservices"},next:{title:"Examples & Recipes",permalink:"/docs/next/category/examples--recipes"}},o={},l=[{value:"When to Use This Approach",id:"when-to-use-this-approach",level:2},{value:"Core Concepts",id:"core-concepts",level:2},{value:"Events",id:"events",level:3},{value:"Producers",id:"producers",level:3},{value:"Subscribers",id:"subscribers",level:3},{value:"Event Handling Providers",id:"event-handling-providers",level:2},{value:"In-Memory Event Bus",id:"in-memory-event-bus",level:3},{value:"MediatR",id:"mediatr",level:3},{value:"MassTransit",id:"masstransit",level:3},{value:"Wolverine",id:"wolverine",level:3},{value:"Subscription Isolation",id:"subscription-isolation",level:2},{value:"Publishing Events",id:"publishing-events",level:2},{value:"Outbox Pattern Considerations",id:"outbox-pattern-considerations",level:2},{value:"Testing Event Handlers",id:"testing-event-handlers",level:2},{value:"Choosing a Provider",id:"choosing-a-provider",level:2},{value:"API Reference",id:"api-reference",level:2}];function c(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,i.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"event-driven-architecture-with-rcommon",children:"Event-Driven Architecture with RCommon"}),"\n",(0,s.jsxs)(n.p,{children:["Event-driven architecture decouples components by having them communicate through events rather than direct calls. Producers emit events without knowing which consumers exist. Consumers react to events without knowing which producer emitted them. RCommon provides a consistent ",(0,s.jsx)(n.code,{children:"IEventProducer"})," / ",(0,s.jsx)(n.code,{children:"ISubscriber"})," abstraction over in-process buses (InMemoryEventBus, MediatR) and distributed brokers (MassTransit, Wolverine)."]}),"\n",(0,s.jsx)(n.h2,{id:"when-to-use-this-approach",children:"When to Use This Approach"}),"\n",(0,s.jsx)(n.p,{children:"Use event-driven design with RCommon when:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Side effects (sending email, updating a read model, audit logging) should not be tangled into command handlers"}),"\n",(0,s.jsx)(n.li,{children:"You want to scale event consumers independently from producers"}),"\n",(0,s.jsx)(n.li,{children:"Components must remain decoupled so they can evolve at different rates"}),"\n",(0,s.jsx)(n.li,{children:"You need reliable delivery guarantees provided by a message broker"}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"core-concepts",children:"Core Concepts"}),"\n",(0,s.jsx)(n.h3,{id:"events",children:"Events"}),"\n",(0,s.jsx)(n.p,{children:"An event represents something that happened. It is named in the past tense and is immutable after construction:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Models.Events;\n\n// An in-process domain event\npublic class LeaveRequestApprovedEvent : ISyncEvent\n{\n public LeaveRequestApprovedEvent() { }\n\n public LeaveRequestApprovedEvent(int leaveRequestId, string employeeId)\n {\n LeaveRequestId = leaveRequestId;\n EmployeeId = employeeId;\n }\n\n public int LeaveRequestId { get; }\n public string EmployeeId { get; }\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"ISyncEvent"})," is the base marker interface. All event types in RCommon derive from it."]}),"\n",(0,s.jsx)(n.h3,{id:"producers",children:"Producers"}),"\n",(0,s.jsxs)(n.p,{children:["A producer publishes an event to one or more subscribers. At the call site, you depend on ",(0,s.jsx)(n.code,{children:"IEventProducer"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public class ApproveLeaveRequestCommandHandler\n : IAppRequestHandler\n{\n private readonly IGraphRepository _repository;\n private readonly IEnumerable _eventProducers;\n\n public ApproveLeaveRequestCommandHandler(\n IGraphRepository repository,\n IEnumerable eventProducers)\n {\n _repository = repository;\n _eventProducers = eventProducers;\n }\n\n public async Task HandleAsync(\n ApproveLeaveRequestCommand request,\n CancellationToken cancellationToken)\n {\n var leaveRequest = await _repository.FindAsync(request.Id);\n leaveRequest.Approved = true;\n await _repository.UpdateAsync(leaveRequest);\n\n var @event = new LeaveRequestApprovedEvent(leaveRequest.Id,\n leaveRequest.RequestingEmployeeId);\n\n foreach (var producer in _eventProducers)\n {\n await producer.ProduceEventAsync(@event);\n }\n\n return new BaseCommandResponse { Success = true };\n }\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"subscribers",children:"Subscribers"}),"\n",(0,s.jsxs)(n.p,{children:["A subscriber reacts to a specific event type. It implements ",(0,s.jsx)(n.code,{children:"ISubscriber"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'public class SendApprovalEmailHandler : ISubscriber\n{\n private readonly IEmailService _emailService;\n\n public SendApprovalEmailHandler(IEmailService emailService)\n {\n _emailService = emailService;\n }\n\n public async Task HandleAsync(\n LeaveRequestApprovedEvent notification,\n CancellationToken cancellationToken = default)\n {\n await _emailService.SendAsync(new EmailRequest\n {\n To = notification.EmployeeId,\n Subject = "Leave Request Approved",\n Body = $"Your leave request #{notification.LeaveRequestId} has been approved."\n });\n }\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"event-handling-providers",children:"Event Handling Providers"}),"\n",(0,s.jsx)(n.p,{children:"RCommon ships with four event handling providers. You choose one (or more) at startup via the fluent builder."}),"\n",(0,s.jsx)(n.admonition,{title:"Modular composition",type:"tip",children:(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"WithEventHandling"})," is cache-aware. In a modular codebase, each feature module can call ",(0,s.jsx)(n.code,{children:"services.AddRCommon().WithEventHandling(eh => eh.AddSubscriber())"})," independently \u2014 the cached ",(0,s.jsx)(n.code,{children:"InMemoryEventBusBuilder"})," is reused across modules and subscriber/producer registrations accumulate. ",(0,s.jsx)(n.code,{children:"AddProducer"})," deduplicates by concrete producer type, so the same producer registered from two modules yields exactly one descriptor. This pattern works for ",(0,s.jsx)(n.code,{children:"InMemoryEventBusBuilder"}),", ",(0,s.jsx)(n.code,{children:"MediatREventHandlingBuilder"}),", and ",(0,s.jsx)(n.code,{children:"WolverineEventHandlingBuilder"}),"; ",(0,s.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"})," has a known cross-module limitation documented on its ",(0,s.jsx)(n.a,{href:"/docs/next/event-handling/masstransit#configuration",children:"MassTransit page"}),". See ",(0,s.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,s.jsx)(n.h3,{id:"in-memory-event-bus",children:"In-Memory Event Bus"}),"\n",(0,s.jsx)(n.p,{children:"The simplest provider. Events are dispatched synchronously within the same process. No external broker required. Best for domain events within a single service."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"services.AddRCommon()\n .WithEventHandling(eventHandling =>\n {\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n });\n"})}),"\n",(0,s.jsx)(n.h3,{id:"mediatr",children:"MediatR"}),"\n",(0,s.jsx)(n.p,{children:"Uses MediatR's notification pipeline. Integrates with existing MediatR configurations and supports MediatR pipeline behaviors:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"services.AddRCommon()\n .WithEventHandling(eventHandling =>\n {\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n });\n"})}),"\n",(0,s.jsx)(n.h3,{id:"masstransit",children:"MassTransit"}),"\n",(0,s.jsx)(n.p,{children:"Durable, distributed messaging. Supports RabbitMQ, Azure Service Bus, Amazon SQS, and others. Events survive process restarts and can be delivered to subscribers in separate services:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'services.AddRCommon()\n .WithEventHandling(eventHandling =>\n {\n eventHandling.UsingRabbitMq((context, cfg) =>\n {\n cfg.Host("rabbitmq://localhost");\n cfg.ConfigureEndpoints(context);\n });\n\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n });\n'})}),"\n",(0,s.jsx)(n.p,{children:"For development and testing, use the in-memory transport instead of RabbitMQ:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"eventHandling.UsingInMemory((context, cfg) =>\n{\n cfg.ConfigureEndpoints(context);\n});\n"})}),"\n",(0,s.jsx)(n.h3,{id:"wolverine",children:"Wolverine"}),"\n",(0,s.jsx)(n.p,{children:"Wolverine focuses on high throughput and local queue processing:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'// In Program.cs, before services.AddRCommon()\nbuilder.Host.UseWolverine(options =>\n{\n options.LocalQueue("leave-events");\n});\n\nservices.AddRCommon()\n .WithEventHandling(eventHandling =>\n {\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n });\n'})}),"\n",(0,s.jsx)(n.h2,{id:"subscription-isolation",children:"Subscription Isolation"}),"\n",(0,s.jsx)(n.p,{children:"When a single service needs both in-process events and cross-service events, register multiple builders. Each builder maintains an independent routing table:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'services.AddRCommon()\n // Internal domain events: stay in-process\n .WithEventHandling(inMemory =>\n {\n inMemory.AddProducer();\n inMemory.AddSubscriber();\n inMemory.AddSubscriber();\n })\n // Cross-service events: leave the process via the broker\n .WithEventHandling(masstransit =>\n {\n masstransit.UsingRabbitMq((context, cfg) =>\n {\n cfg.Host("rabbitmq://localhost");\n cfg.ConfigureEndpoints(context);\n });\n masstransit.AddProducer();\n masstransit.AddSubscriber();\n });\n'})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"LeaveRequestApprovedEvent"})," is only handled by the InMemoryEventBus producers. ",(0,s.jsx)(n.code,{children:"PayrollSyncRequiredEvent"})," is only handled by MassTransit producers. Events do not cross builder boundaries unless the same event type and handler are registered with both builders."]}),"\n",(0,s.jsx)(n.p,{children:"The subscription isolation example from the RCommon Examples project demonstrates this with three event types:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"InMemoryOnlyEvent"})," \u2014 subscribed only to ",(0,s.jsx)(n.code,{children:"InMemoryEventBusBuilder"}),", ignored by MassTransit producers"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"MassTransitOnlyEvent"})," \u2014 subscribed only to ",(0,s.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"}),", ignored by in-memory producers"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"SharedEvent"})," \u2014 subscribed to both builders, handled by both producer types"]}),"\n"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"services.AddRCommon()\n .WithEventHandling(eventHandling =>\n {\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n eventHandling.AddSubscriber();\n })\n .WithEventHandling(eventHandling =>\n {\n eventHandling.UsingInMemory((context, cfg) =>\n {\n cfg.ConfigureEndpoints(context);\n });\n eventHandling.AddProducer();\n eventHandling.AddSubscriber();\n eventHandling.AddSubscriber();\n });\n"})}),"\n",(0,s.jsx)(n.h2,{id:"publishing-events",children:"Publishing Events"}),"\n",(0,s.jsxs)(n.p,{children:["Regardless of which provider is configured, publishing always uses the same ",(0,s.jsx)(n.code,{children:"IEventProducer"})," abstraction. Inject ",(0,s.jsx)(n.code,{children:"IEnumerable"})," to publish to all registered producers:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"// From a background worker\npublic class Worker : BackgroundService\n{\n private readonly IServiceProvider _serviceProvider;\n\n public Worker(IServiceProvider serviceProvider)\n => _serviceProvider = serviceProvider;\n\n protected override async Task ExecuteAsync(CancellationToken stoppingToken)\n {\n var eventProducers = _serviceProvider.GetServices();\n var @event = new TestEvent(DateTime.UtcNow, Guid.NewGuid());\n\n foreach (var producer in eventProducers)\n {\n await producer.ProduceEventAsync(@event);\n }\n }\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"outbox-pattern-considerations",children:"Outbox Pattern Considerations"}),"\n",(0,s.jsxs)(n.p,{children:["When an event must be published only if the database transaction succeeds, use the unit-of-work pipeline together with event publishing. RCommon's ",(0,s.jsx)(n.code,{children:"WithUnitOfWorkToRequestPipeline()"})," wraps each mediator request in a transaction scope. Publish the event after the state change within the same handler so the event and the state change succeed or fail together:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:".WithMediator(mediator =>\n{\n mediator.AddUnitOfWorkToRequestPipeline(); // wraps handlers in a transaction\n})\n.WithUnitOfWork(unitOfWork =>\n{\n unitOfWork.SetOptions(options =>\n {\n options.AutoCompleteScope = true;\n options.DefaultIsolation = IsolationLevel.ReadCommitted;\n });\n})\n"})}),"\n",(0,s.jsx)(n.p,{children:"For full outbox durability (where events survive process crashes between the write and the publish), integrate the MassTransit outbox or Wolverine's outbox support at the transport layer."}),"\n",(0,s.jsx)(n.h2,{id:"testing-event-handlers",children:"Testing Event Handlers"}),"\n",(0,s.jsx)(n.p,{children:"Event handlers are ordinary classes with injected dependencies. Testing is straightforward:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'[Test]\npublic async Task SendApprovalEmailHandler_Sends_Email_On_Approval()\n{\n var emailServiceMock = new Mock();\n var handler = new SendApprovalEmailHandler(emailServiceMock.Object);\n\n await handler.HandleAsync(\n new LeaveRequestApprovedEvent(leaveRequestId: 42, employeeId: "emp-001"),\n CancellationToken.None);\n\n emailServiceMock.Verify(\n x => x.SendAsync(It.Is(r => r.To == "emp-001")),\n Times.Once);\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"choosing-a-provider",children:"Choosing a Provider"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Scenario"}),(0,s.jsx)(n.th,{children:"Recommended Provider"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Single-service, in-process only"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"InMemoryEventBusBuilder"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Existing MediatR codebase"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MediatREventHandlingBuilder"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Distributed, cross-service, high reliability"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"High-throughput local queues, Wolverine ecosystem"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Mixed: in-process + distributed"}),(0,s.jsx)(n.td,{children:"Multiple builders with subscription isolation"})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:"api-reference",children:"API Reference"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Package"}),(0,s.jsx)(n.th,{children:"Purpose"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"ISyncEvent"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.Models"})}),(0,s.jsx)(n.td,{children:"Base marker interface for all events"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"IEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(n.td,{children:"Publishes events to subscribers"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"ISubscriber"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(n.td,{children:"Receives and handles events"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"InMemoryEventBusBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(n.td,{children:"Configures the in-process event bus"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MediatREventHandlingBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MediatR"})}),(0,s.jsx)(n.td,{children:"Configures MediatR-backed event handling"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,s.jsx)(n.td,{children:"Configures MassTransit-backed event handling"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.Wolverine"})}),(0,s.jsx)(n.td,{children:"Configures Wolverine-backed event handling"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithEventBusEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(n.td,{children:"In-memory producer implementation"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithMediatREventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MediatR"})}),(0,s.jsx)(n.td,{children:"MediatR producer implementation"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,s.jsx)(n.td,{children:"MassTransit producer implementation"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.Wolverine"})}),(0,s.jsx)(n.td,{children:"Wolverine producer implementation"})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},64996(e,n,r){r.d(n,{A:()=>d});r(30758);var s=r(13526);const i="tabItem_jA3u";var t=r(86070);function d({children:e,hidden:n,className:r}){return(0,t.jsx)("div",{role:"tabpanel",className:(0,s.A)(i,r),hidden:n,children:e})}},85363(e,n,r){r.d(n,{A:()=>R});var s=r(30758),i=r(13526),t=r(32416),d=r(25557),a=r(85924),o=r(64493),l=r(10274),c=r(44118);function u(e){return s.Children.toArray(e).filter(e=>"\n"!==e).map(e=>{if(!e||(0,s.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)})?.filter(Boolean)??[]}function h(e){const{values:n,children:r}=e;return(0,s.useMemo)(()=>{const e=n??function(e){return u(e).map(({props:{value:e,label:n,attributes:r,default:s}})=>({value:e,label:n,attributes:r,default:s}))}(r);return function(e){const n=(0,l.X)(e,(e,n)=>e.value===n.value);if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map(e=>e.value).join(", ")}" found in . Every value needs to be unique.`)}(e),e},[n,r])}function v({value:e,tabValues:n}){return n.some(n=>n.value===e)}function p({queryString:e=!1,groupId:n}){const r=(0,d.W6)(),i=function({queryString:e=!1,groupId:n}){if("string"==typeof e)return e;if(!1===e)return null;if(!0===e&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:e,groupId:n});return[(0,o.aZ)(i),(0,s.useCallback)(e=>{if(!i)return;const n=new URLSearchParams(r.location.search);n.set(i,e),r.replace({...r.location,search:n.toString()})},[i,r])]}function m(e){const{defaultValue:n,queryString:r=!1,groupId:i}=e,t=h(e),[d,o]=(0,s.useState)(()=>function({defaultValue:e,tabValues:n}){if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(e){if(!v({value:e,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${e}" but none of its children has the corresponding value. Available values are: ${n.map(e=>e.value).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return e}const r=n.find(e=>e.default)??n[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:n,tabValues:t})),[l,u]=p({queryString:r,groupId:i}),[m,b]=function({groupId:e}){const n=function(e){return e?`docusaurus.tab.${e}`:null}(e),[r,i]=(0,c.Dv)(n);return[r,(0,s.useCallback)(e=>{n&&i.set(e)},[n,i])]}({groupId:i}),x=(()=>{const e=l??m;return v({value:e,tabValues:t})?e:null})();(0,a.A)(()=>{x&&o(x)},[x]);return{selectedValue:d,selectValue:(0,s.useCallback)(e=>{if(!v({value:e,tabValues:t}))throw new Error(`Can't select invalid tab value=${e}`);o(e),u(e),b(e)},[u,b,t]),tabValues:t}}var b=r(81264);const x="tabList_TfjZ",g="tabItem_vVs9";var j=r(86070);function E({className:e,block:n,selectedValue:r,selectValue:s,tabValues:d}){const a=[],{blockElementScrollPositionUntilNextRender:o}=(0,t.a_)(),l=e=>{const n=e.currentTarget,i=a.indexOf(n),t=d[i].value;t!==r&&(o(n),s(t))},c=e=>{let n=null;switch(e.key){case"Enter":l(e);break;case"ArrowRight":{const r=a.indexOf(e.currentTarget)+1;n=a[r]??a[0];break}case"ArrowLeft":{const r=a.indexOf(e.currentTarget)-1;n=a[r]??a[a.length-1];break}}n?.focus()};return(0,j.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,i.A)("tabs",{"tabs--block":n},e),children:d.map(({value:e,label:n,attributes:s})=>(0,j.jsx)("li",{role:"tab",tabIndex:r===e?0:-1,"aria-selected":r===e,ref:e=>a.push(e),onKeyDown:c,onClick:l,...s,className:(0,i.A)("tabs__item",g,s?.className,{"tabs__item--active":r===e}),children:n??e},e))})}function y({lazy:e,children:n,selectedValue:r}){const i=(Array.isArray(n)?n:[n]).filter(Boolean);if(e){const e=i.find(e=>e.props.value===r);return e?(0,s.cloneElement)(e,{className:"margin-top--md"}):null}return(0,j.jsx)("div",{className:"margin-top--md",children:i.map((e,n)=>(0,s.cloneElement)(e,{key:n,hidden:e.props.value!==r}))})}function f(e){const n=m(e);return(0,j.jsxs)("div",{className:(0,i.A)("tabs-container",x),children:[(0,j.jsx)(E,{...n,...e}),(0,j.jsx)(y,{...n,...e})]})}function R(e){const n=(0,b.A)();return(0,j.jsx)(f,{...e,children:u(e.children)},String(n))}},81753(e,n,r){r.d(n,{R:()=>d,x:()=>a});var s=r(30758);const i={},t=s.createContext(i);function d(e){const n=s.useContext(t);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:d(e.components),s.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/8b219ab3.219ee8ba.js b/website/build/assets/js/8b219ab3.219ee8ba.js deleted file mode 100644 index 8e4f68f0..00000000 --- a/website/build/assets/js/8b219ab3.219ee8ba.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[3709],{50631(e,n,r){r.r(n),r.d(n,{assets:()=>o,contentTitle:()=>d,default:()=>u,frontMatter:()=>i,metadata:()=>a,toc:()=>l});var s=r(86070),t=r(81753);r(85363),r(64996);const i={title:"Event-Driven Architecture",sidebar_position:3,description:"Implement event-driven .NET apps with RCommon using IEventProducer and ISubscriber across in-memory, MediatR, MassTransit, and Wolverine providers."},d="Event-Driven Architecture with RCommon",a={id:"architecture-guides/event-driven",title:"Event-Driven Architecture",description:"Implement event-driven .NET apps with RCommon using IEventProducer and ISubscriber across in-memory, MediatR, MassTransit, and Wolverine providers.",source:"@site/docs/architecture-guides/event-driven.mdx",sourceDirName:"architecture-guides",slug:"/architecture-guides/event-driven",permalink:"/docs/next/architecture-guides/event-driven",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/architecture-guides/event-driven.mdx",tags:[],version:"current",sidebarPosition:3,frontMatter:{title:"Event-Driven Architecture",sidebar_position:3,description:"Implement event-driven .NET apps with RCommon using IEventProducer and ISubscriber across in-memory, MediatR, MassTransit, and Wolverine providers."},sidebar:"docsSidebar",previous:{title:"Microservices",permalink:"/docs/next/architecture-guides/microservices"},next:{title:"Examples & Recipes",permalink:"/docs/next/category/examples--recipes"}},o={},l=[{value:"When to Use This Approach",id:"when-to-use-this-approach",level:2},{value:"Core Concepts",id:"core-concepts",level:2},{value:"Events",id:"events",level:3},{value:"Producers",id:"producers",level:3},{value:"Subscribers",id:"subscribers",level:3},{value:"Event Handling Providers",id:"event-handling-providers",level:2},{value:"In-Memory Event Bus",id:"in-memory-event-bus",level:3},{value:"MediatR",id:"mediatr",level:3},{value:"MassTransit",id:"masstransit",level:3},{value:"Wolverine",id:"wolverine",level:3},{value:"Subscription Isolation",id:"subscription-isolation",level:2},{value:"Publishing Events",id:"publishing-events",level:2},{value:"Outbox Pattern Considerations",id:"outbox-pattern-considerations",level:2},{value:"Testing Event Handlers",id:"testing-event-handlers",level:2},{value:"Choosing a Provider",id:"choosing-a-provider",level:2},{value:"API Reference",id:"api-reference",level:2}];function c(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"event-driven-architecture-with-rcommon",children:"Event-Driven Architecture with RCommon"}),"\n",(0,s.jsxs)(n.p,{children:["Event-driven architecture decouples components by having them communicate through events rather than direct calls. Producers emit events without knowing which consumers exist. Consumers react to events without knowing which producer emitted them. RCommon provides a consistent ",(0,s.jsx)(n.code,{children:"IEventProducer"})," / ",(0,s.jsx)(n.code,{children:"ISubscriber"})," abstraction over in-process buses (InMemoryEventBus, MediatR) and distributed brokers (MassTransit, Wolverine)."]}),"\n",(0,s.jsx)(n.h2,{id:"when-to-use-this-approach",children:"When to Use This Approach"}),"\n",(0,s.jsx)(n.p,{children:"Use event-driven design with RCommon when:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:"Side effects (sending email, updating a read model, audit logging) should not be tangled into command handlers"}),"\n",(0,s.jsx)(n.li,{children:"You want to scale event consumers independently from producers"}),"\n",(0,s.jsx)(n.li,{children:"Components must remain decoupled so they can evolve at different rates"}),"\n",(0,s.jsx)(n.li,{children:"You need reliable delivery guarantees provided by a message broker"}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"core-concepts",children:"Core Concepts"}),"\n",(0,s.jsx)(n.h3,{id:"events",children:"Events"}),"\n",(0,s.jsx)(n.p,{children:"An event represents something that happened. It is named in the past tense and is immutable after construction:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Models.Events;\r\n\r\n// An in-process domain event\r\npublic class LeaveRequestApprovedEvent : ISyncEvent\r\n{\r\n public LeaveRequestApprovedEvent() { }\r\n\r\n public LeaveRequestApprovedEvent(int leaveRequestId, string employeeId)\r\n {\r\n LeaveRequestId = leaveRequestId;\r\n EmployeeId = employeeId;\r\n }\r\n\r\n public int LeaveRequestId { get; }\r\n public string EmployeeId { get; }\r\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"ISyncEvent"})," is the base marker interface. All event types in RCommon derive from it."]}),"\n",(0,s.jsx)(n.h3,{id:"producers",children:"Producers"}),"\n",(0,s.jsxs)(n.p,{children:["A producer publishes an event to one or more subscribers. At the call site, you depend on ",(0,s.jsx)(n.code,{children:"IEventProducer"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public class ApproveLeaveRequestCommandHandler\r\n : IAppRequestHandler\r\n{\r\n private readonly IGraphRepository _repository;\r\n private readonly IEnumerable _eventProducers;\r\n\r\n public ApproveLeaveRequestCommandHandler(\r\n IGraphRepository repository,\r\n IEnumerable eventProducers)\r\n {\r\n _repository = repository;\r\n _eventProducers = eventProducers;\r\n }\r\n\r\n public async Task HandleAsync(\r\n ApproveLeaveRequestCommand request,\r\n CancellationToken cancellationToken)\r\n {\r\n var leaveRequest = await _repository.FindAsync(request.Id);\r\n leaveRequest.Approved = true;\r\n await _repository.UpdateAsync(leaveRequest);\r\n\r\n var @event = new LeaveRequestApprovedEvent(leaveRequest.Id,\r\n leaveRequest.RequestingEmployeeId);\r\n\r\n foreach (var producer in _eventProducers)\r\n {\r\n await producer.ProduceEventAsync(@event);\r\n }\r\n\r\n return new BaseCommandResponse { Success = true };\r\n }\r\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"subscribers",children:"Subscribers"}),"\n",(0,s.jsxs)(n.p,{children:["A subscriber reacts to a specific event type. It implements ",(0,s.jsx)(n.code,{children:"ISubscriber"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'public class SendApprovalEmailHandler : ISubscriber\r\n{\r\n private readonly IEmailService _emailService;\r\n\r\n public SendApprovalEmailHandler(IEmailService emailService)\r\n {\r\n _emailService = emailService;\r\n }\r\n\r\n public async Task HandleAsync(\r\n LeaveRequestApprovedEvent notification,\r\n CancellationToken cancellationToken = default)\r\n {\r\n await _emailService.SendAsync(new EmailRequest\r\n {\r\n To = notification.EmployeeId,\r\n Subject = "Leave Request Approved",\r\n Body = $"Your leave request #{notification.LeaveRequestId} has been approved."\r\n });\r\n }\r\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"event-handling-providers",children:"Event Handling Providers"}),"\n",(0,s.jsx)(n.p,{children:"RCommon ships with four event handling providers. You choose one (or more) at startup via the fluent builder."}),"\n",(0,s.jsx)(n.h3,{id:"in-memory-event-bus",children:"In-Memory Event Bus"}),"\n",(0,s.jsx)(n.p,{children:"The simplest provider. Events are dispatched synchronously within the same process. No external broker required. Best for domain events within a single service."}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"services.AddRCommon()\r\n .WithEventHandling(eventHandling =>\r\n {\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n });\n"})}),"\n",(0,s.jsx)(n.h3,{id:"mediatr",children:"MediatR"}),"\n",(0,s.jsx)(n.p,{children:"Uses MediatR's notification pipeline. Integrates with existing MediatR configurations and supports MediatR pipeline behaviors:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"services.AddRCommon()\r\n .WithEventHandling(eventHandling =>\r\n {\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n });\n"})}),"\n",(0,s.jsx)(n.h3,{id:"masstransit",children:"MassTransit"}),"\n",(0,s.jsx)(n.p,{children:"Durable, distributed messaging. Supports RabbitMQ, Azure Service Bus, Amazon SQS, and others. Events survive process restarts and can be delivered to subscribers in separate services:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'services.AddRCommon()\r\n .WithEventHandling(eventHandling =>\r\n {\r\n eventHandling.UsingRabbitMq((context, cfg) =>\r\n {\r\n cfg.Host("rabbitmq://localhost");\r\n cfg.ConfigureEndpoints(context);\r\n });\r\n\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n });\n'})}),"\n",(0,s.jsx)(n.p,{children:"For development and testing, use the in-memory transport instead of RabbitMQ:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"eventHandling.UsingInMemory((context, cfg) =>\r\n{\r\n cfg.ConfigureEndpoints(context);\r\n});\n"})}),"\n",(0,s.jsx)(n.h3,{id:"wolverine",children:"Wolverine"}),"\n",(0,s.jsx)(n.p,{children:"Wolverine focuses on high throughput and local queue processing:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'// In Program.cs, before services.AddRCommon()\r\nbuilder.Host.UseWolverine(options =>\r\n{\r\n options.LocalQueue("leave-events");\r\n});\r\n\r\nservices.AddRCommon()\r\n .WithEventHandling(eventHandling =>\r\n {\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n });\n'})}),"\n",(0,s.jsx)(n.h2,{id:"subscription-isolation",children:"Subscription Isolation"}),"\n",(0,s.jsx)(n.p,{children:"When a single service needs both in-process events and cross-service events, register multiple builders. Each builder maintains an independent routing table:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'services.AddRCommon()\r\n // Internal domain events: stay in-process\r\n .WithEventHandling(inMemory =>\r\n {\r\n inMemory.AddProducer();\r\n inMemory.AddSubscriber();\r\n inMemory.AddSubscriber();\r\n })\r\n // Cross-service events: leave the process via the broker\r\n .WithEventHandling(masstransit =>\r\n {\r\n masstransit.UsingRabbitMq((context, cfg) =>\r\n {\r\n cfg.Host("rabbitmq://localhost");\r\n cfg.ConfigureEndpoints(context);\r\n });\r\n masstransit.AddProducer();\r\n masstransit.AddSubscriber();\r\n });\n'})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"LeaveRequestApprovedEvent"})," is only handled by the InMemoryEventBus producers. ",(0,s.jsx)(n.code,{children:"PayrollSyncRequiredEvent"})," is only handled by MassTransit producers. Events do not cross builder boundaries unless the same event type and handler are registered with both builders."]}),"\n",(0,s.jsx)(n.p,{children:"The subscription isolation example from the RCommon Examples project demonstrates this with three event types:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"InMemoryOnlyEvent"})," \u2014 subscribed only to ",(0,s.jsx)(n.code,{children:"InMemoryEventBusBuilder"}),", ignored by MassTransit producers"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"MassTransitOnlyEvent"})," \u2014 subscribed only to ",(0,s.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"}),", ignored by in-memory producers"]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"SharedEvent"})," \u2014 subscribed to both builders, handled by both producer types"]}),"\n"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"services.AddRCommon()\r\n .WithEventHandling(eventHandling =>\r\n {\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n eventHandling.AddSubscriber();\r\n })\r\n .WithEventHandling(eventHandling =>\r\n {\r\n eventHandling.UsingInMemory((context, cfg) =>\r\n {\r\n cfg.ConfigureEndpoints(context);\r\n });\r\n eventHandling.AddProducer();\r\n eventHandling.AddSubscriber();\r\n eventHandling.AddSubscriber();\r\n });\n"})}),"\n",(0,s.jsx)(n.h2,{id:"publishing-events",children:"Publishing Events"}),"\n",(0,s.jsxs)(n.p,{children:["Regardless of which provider is configured, publishing always uses the same ",(0,s.jsx)(n.code,{children:"IEventProducer"})," abstraction. Inject ",(0,s.jsx)(n.code,{children:"IEnumerable"})," to publish to all registered producers:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"// From a background worker\r\npublic class Worker : BackgroundService\r\n{\r\n private readonly IServiceProvider _serviceProvider;\r\n\r\n public Worker(IServiceProvider serviceProvider)\r\n => _serviceProvider = serviceProvider;\r\n\r\n protected override async Task ExecuteAsync(CancellationToken stoppingToken)\r\n {\r\n var eventProducers = _serviceProvider.GetServices();\r\n var @event = new TestEvent(DateTime.UtcNow, Guid.NewGuid());\r\n\r\n foreach (var producer in eventProducers)\r\n {\r\n await producer.ProduceEventAsync(@event);\r\n }\r\n }\r\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"outbox-pattern-considerations",children:"Outbox Pattern Considerations"}),"\n",(0,s.jsxs)(n.p,{children:["When an event must be published only if the database transaction succeeds, use the unit-of-work pipeline together with event publishing. RCommon's ",(0,s.jsx)(n.code,{children:"WithUnitOfWorkToRequestPipeline()"})," wraps each mediator request in a transaction scope. Publish the event after the state change within the same handler so the event and the state change succeed or fail together:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:".WithMediator(mediator =>\r\n{\r\n mediator.AddUnitOfWorkToRequestPipeline(); // wraps handlers in a transaction\r\n})\r\n.WithUnitOfWork(unitOfWork =>\r\n{\r\n unitOfWork.SetOptions(options =>\r\n {\r\n options.AutoCompleteScope = true;\r\n options.DefaultIsolation = IsolationLevel.ReadCommitted;\r\n });\r\n})\n"})}),"\n",(0,s.jsx)(n.p,{children:"For full outbox durability (where events survive process crashes between the write and the publish), integrate the MassTransit outbox or Wolverine's outbox support at the transport layer."}),"\n",(0,s.jsx)(n.h2,{id:"testing-event-handlers",children:"Testing Event Handlers"}),"\n",(0,s.jsx)(n.p,{children:"Event handlers are ordinary classes with injected dependencies. Testing is straightforward:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'[Test]\r\npublic async Task SendApprovalEmailHandler_Sends_Email_On_Approval()\r\n{\r\n var emailServiceMock = new Mock();\r\n var handler = new SendApprovalEmailHandler(emailServiceMock.Object);\r\n\r\n await handler.HandleAsync(\r\n new LeaveRequestApprovedEvent(leaveRequestId: 42, employeeId: "emp-001"),\r\n CancellationToken.None);\r\n\r\n emailServiceMock.Verify(\r\n x => x.SendAsync(It.Is(r => r.To == "emp-001")),\r\n Times.Once);\r\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"choosing-a-provider",children:"Choosing a Provider"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Scenario"}),(0,s.jsx)(n.th,{children:"Recommended Provider"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Single-service, in-process only"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"InMemoryEventBusBuilder"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Existing MediatR codebase"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MediatREventHandlingBuilder"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Distributed, cross-service, high reliability"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"High-throughput local queues, Wolverine ecosystem"}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:"Mixed: in-process + distributed"}),(0,s.jsx)(n.td,{children:"Multiple builders with subscription isolation"})]})]})]}),"\n",(0,s.jsx)(n.h2,{id:"api-reference",children:"API Reference"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Package"}),(0,s.jsx)(n.th,{children:"Purpose"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"ISyncEvent"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.Models"})}),(0,s.jsx)(n.td,{children:"Base marker interface for all events"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"IEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(n.td,{children:"Publishes events to subscribers"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"ISubscriber"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(n.td,{children:"Receives and handles events"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"InMemoryEventBusBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(n.td,{children:"Configures the in-process event bus"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MediatREventHandlingBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MediatR"})}),(0,s.jsx)(n.td,{children:"Configures MediatR-backed event handling"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,s.jsx)(n.td,{children:"Configures MassTransit-backed event handling"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.Wolverine"})}),(0,s.jsx)(n.td,{children:"Configures Wolverine-backed event handling"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithEventBusEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.EventHandling"})}),(0,s.jsx)(n.td,{children:"In-memory producer implementation"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithMediatREventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MediatR"})}),(0,s.jsx)(n.td,{children:"MediatR producer implementation"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,s.jsx)(n.td,{children:"MassTransit producer implementation"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.Wolverine"})}),(0,s.jsx)(n.td,{children:"Wolverine producer implementation"})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},64996(e,n,r){r.d(n,{A:()=>d});r(30758);var s=r(13526);const t="tabItem_jA3u";var i=r(86070);function d({children:e,hidden:n,className:r}){return(0,i.jsx)("div",{role:"tabpanel",className:(0,s.A)(t,r),hidden:n,children:e})}},85363(e,n,r){r.d(n,{A:()=>R});var s=r(30758),t=r(13526),i=r(32416),d=r(25557),a=r(85924),o=r(64493),l=r(10274),c=r(44118);function u(e){return s.Children.toArray(e).filter(e=>"\n"!==e).map(e=>{if(!e||(0,s.isValidElement)(e)&&function(e){const{props:n}=e;return!!n&&"object"==typeof n&&"value"in n}(e))return e;throw new Error(`Docusaurus error: Bad child <${"string"==typeof e.type?e.type:e.type.name}>: all children of the component should be , and every should have a unique "value" prop.`)})?.filter(Boolean)??[]}function h(e){const{values:n,children:r}=e;return(0,s.useMemo)(()=>{const e=n??function(e){return u(e).map(({props:{value:e,label:n,attributes:r,default:s}})=>({value:e,label:n,attributes:r,default:s}))}(r);return function(e){const n=(0,l.X)(e,(e,n)=>e.value===n.value);if(n.length>0)throw new Error(`Docusaurus error: Duplicate values "${n.map(e=>e.value).join(", ")}" found in . Every value needs to be unique.`)}(e),e},[n,r])}function v({value:e,tabValues:n}){return n.some(n=>n.value===e)}function p({queryString:e=!1,groupId:n}){const r=(0,d.W6)(),t=function({queryString:e=!1,groupId:n}){if("string"==typeof e)return e;if(!1===e)return null;if(!0===e&&!n)throw new Error('Docusaurus error: The component groupId prop is required if queryString=true, because this value is used as the search param name. You can also provide an explicit value such as queryString="my-search-param".');return n??null}({queryString:e,groupId:n});return[(0,o.aZ)(t),(0,s.useCallback)(e=>{if(!t)return;const n=new URLSearchParams(r.location.search);n.set(t,e),r.replace({...r.location,search:n.toString()})},[t,r])]}function m(e){const{defaultValue:n,queryString:r=!1,groupId:t}=e,i=h(e),[d,o]=(0,s.useState)(()=>function({defaultValue:e,tabValues:n}){if(0===n.length)throw new Error("Docusaurus error: the component requires at least one children component");if(e){if(!v({value:e,tabValues:n}))throw new Error(`Docusaurus error: The has a defaultValue "${e}" but none of its children has the corresponding value. Available values are: ${n.map(e=>e.value).join(", ")}. If you intend to show no default tab, use defaultValue={null} instead.`);return e}const r=n.find(e=>e.default)??n[0];if(!r)throw new Error("Unexpected error: 0 tabValues");return r.value}({defaultValue:n,tabValues:i})),[l,u]=p({queryString:r,groupId:t}),[m,b]=function({groupId:e}){const n=function(e){return e?`docusaurus.tab.${e}`:null}(e),[r,t]=(0,c.Dv)(n);return[r,(0,s.useCallback)(e=>{n&&t.set(e)},[n,t])]}({groupId:t}),x=(()=>{const e=l??m;return v({value:e,tabValues:i})?e:null})();(0,a.A)(()=>{x&&o(x)},[x]);return{selectedValue:d,selectValue:(0,s.useCallback)(e=>{if(!v({value:e,tabValues:i}))throw new Error(`Can't select invalid tab value=${e}`);o(e),u(e),b(e)},[u,b,i]),tabValues:i}}var b=r(81264);const x="tabList_TfjZ",g="tabItem_vVs9";var j=r(86070);function E({className:e,block:n,selectedValue:r,selectValue:s,tabValues:d}){const a=[],{blockElementScrollPositionUntilNextRender:o}=(0,i.a_)(),l=e=>{const n=e.currentTarget,t=a.indexOf(n),i=d[t].value;i!==r&&(o(n),s(i))},c=e=>{let n=null;switch(e.key){case"Enter":l(e);break;case"ArrowRight":{const r=a.indexOf(e.currentTarget)+1;n=a[r]??a[0];break}case"ArrowLeft":{const r=a.indexOf(e.currentTarget)-1;n=a[r]??a[a.length-1];break}}n?.focus()};return(0,j.jsx)("ul",{role:"tablist","aria-orientation":"horizontal",className:(0,t.A)("tabs",{"tabs--block":n},e),children:d.map(({value:e,label:n,attributes:s})=>(0,j.jsx)("li",{role:"tab",tabIndex:r===e?0:-1,"aria-selected":r===e,ref:e=>a.push(e),onKeyDown:c,onClick:l,...s,className:(0,t.A)("tabs__item",g,s?.className,{"tabs__item--active":r===e}),children:n??e},e))})}function y({lazy:e,children:n,selectedValue:r}){const t=(Array.isArray(n)?n:[n]).filter(Boolean);if(e){const e=t.find(e=>e.props.value===r);return e?(0,s.cloneElement)(e,{className:"margin-top--md"}):null}return(0,j.jsx)("div",{className:"margin-top--md",children:t.map((e,n)=>(0,s.cloneElement)(e,{key:n,hidden:e.props.value!==r}))})}function f(e){const n=m(e);return(0,j.jsxs)("div",{className:(0,t.A)("tabs-container",x),children:[(0,j.jsx)(E,{...n,...e}),(0,j.jsx)(y,{...n,...e})]})}function R(e){const n=(0,b.A)();return(0,j.jsx)(f,{...e,children:u(e.children)},String(n))}},81753(e,n,r){r.d(n,{R:()=>d,x:()=>a});var s=r(30758);const t={},i=s.createContext(t);function d(e){const n=s.useContext(i);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:d(e.components),s.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/a283db28.868ee359.js b/website/build/assets/js/a283db28.868ee359.js deleted file mode 100644 index dc54a311..00000000 --- a/website/build/assets/js/a283db28.868ee359.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[5585],{99870(e,n,i){i.r(n),i.d(n,{assets:()=>c,contentTitle:()=>l,default:()=>h,frontMatter:()=>t,metadata:()=>d,toc:()=>o});var r=i(86070),s=i(81753),a=i(75783);const t={title:"Overview",sidebar_position:1,description:"Provider-agnostic email abstraction for .NET with IEmailService, built-in SMTP support via SmtpClient, HTML bodies, attachments, and an EmailSent event."},l="Email Overview",d={id:"email/overview",title:"Overview",description:"Provider-agnostic email abstraction for .NET with IEmailService, built-in SMTP support via SmtpClient, HTML bodies, attachments, and an EmailSent event.",source:"@site/docs/email/overview.mdx",sourceDirName:"email",slug:"/email/overview",permalink:"/docs/next/email/overview",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/email/overview.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Overview",sidebar_position:1,description:"Provider-agnostic email abstraction for .NET with IEmailService, built-in SMTP support via SmtpClient, HTML bodies, attachments, and an EmailSent event."},sidebar:"docsSidebar",previous:{title:"Email",permalink:"/docs/next/category/email"},next:{title:"SendGrid",permalink:"/docs/next/email/sendgrid"}},c={},o=[{value:"Overview",id:"overview",level:2},{value:"Core abstractions",id:"core-abstractions",level:3},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Usage",id:"usage",level:2},{value:"Sending a plain-text email",id:"sending-a-plain-text-email",level:3},{value:"Sending an HTML email",id:"sending-an-html-email",level:3},{value:"Sending with an attachment",id:"sending-with-an-attachment",level:3},{value:"Subscribing to the EmailSent event",id:"subscribing-to-the-emailsent-event",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"IEmailService",id:"iemailservice",level:3},{value:"SmtpEmailSettings",id:"smtpemailsettings",level:3},{value:"IRCommonBuilder extension",id:"ircommonbuilder-extension",level:3}];function m(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"email-overview",children:"Email Overview"}),"\n",(0,r.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,r.jsxs)(n.p,{children:["RCommon provides a provider-agnostic email abstraction so that application code does not depend on a specific mail delivery library. Your services depend only on ",(0,r.jsx)(n.code,{children:"IEmailService"}),"; the underlying transport \u2014 SMTP or a cloud API \u2014 is a startup configuration concern."]}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"RCommon.Emailing"})," package delivers a built-in SMTP implementation backed by ",(0,r.jsx)(n.code,{children:"System.Net.Mail.SmtpClient"}),". Third-party providers such as SendGrid are available through separate packages."]}),"\n",(0,r.jsx)(n.h3,{id:"core-abstractions",children:"Core abstractions"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"IEmailService"})," exposes two methods and one event:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"SendEmail(MailMessage)"})," \u2014 synchronous send."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"SendEmailAsync(MailMessage, CancellationToken)"})," \u2014 asynchronous send."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"EmailSent"})," event \u2014 raised after a message is sent successfully, with ",(0,r.jsx)(n.code,{children:"EmailEventArgs"})," carrying the original ",(0,r.jsx)(n.code,{children:"MailMessage"}),"."]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["Both methods accept a standard ",(0,r.jsx)(n.code,{children:"System.Net.Mail.MailMessage"}),", which means you can use the full .NET mail model (multiple recipients, CC, BCC, attachments, HTML body, inline images) regardless of which provider is configured."]}),"\n",(0,r.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(a.A,{packageName:"RCommon.Emailing"}),"\n",(0,r.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsxs)(n.p,{children:["Call ",(0,r.jsx)(n.code,{children:"WithSmtpEmailServices"})," inside your ",(0,r.jsx)(n.code,{children:"AddRCommon()"})," block and provide a delegate that configures ",(0,r.jsx)(n.code,{children:"SmtpEmailSettings"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithSmtpEmailServices(smtp =>\r\n {\r\n smtp.Host = "smtp.example.com";\r\n smtp.Port = 587;\r\n smtp.EnableSsl = true;\r\n smtp.UserName = builder.Configuration["Email:UserName"];\r\n smtp.Password = builder.Configuration["Email:Password"];\r\n smtp.FromEmailDefault = "noreply@example.com";\r\n smtp.FromNameDefault = "My Application";\r\n });\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Bind settings from ",(0,r.jsx)(n.code,{children:"appsettings.json"})," to avoid hard-coding values:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\r\n "Email": {\r\n "Host": "smtp.example.com",\r\n "Port": 587,\r\n "EnableSsl": true,\r\n "UserName": "",\r\n "Password": "",\r\n "FromEmailDefault": "noreply@example.com",\r\n "FromNameDefault": "My Application"\r\n }\r\n}\n'})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'builder.Services.AddRCommon()\r\n .WithSmtpEmailServices(smtp =>\r\n builder.Configuration.GetSection("Email").Bind(smtp));\n'})}),"\n",(0,r.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,r.jsxs)(n.p,{children:["Inject ",(0,r.jsx)(n.code,{children:"IEmailService"})," wherever you need to send mail."]}),"\n",(0,r.jsx)(n.h3,{id:"sending-a-plain-text-email",children:"Sending a plain-text email"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using System.Net.Mail;\r\n\r\npublic class WelcomeEmailService\r\n{\r\n private readonly IEmailService _emailService;\r\n\r\n public WelcomeEmailService(IEmailService emailService)\r\n {\r\n _emailService = emailService;\r\n }\r\n\r\n public async Task SendWelcomeAsync(string recipientEmail,\r\n string recipientName, CancellationToken ct = default)\r\n {\r\n using var message = new MailMessage(\r\n from: "noreply@example.com",\r\n to: recipientEmail,\r\n subject: "Welcome to the platform",\r\n body: $"Hi {recipientName}, your account is ready.");\r\n\r\n await _emailService.SendEmailAsync(message, ct);\r\n }\r\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"sending-an-html-email",children:"Sending an HTML email"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using var message = new MailMessage\r\n{\r\n From = new MailAddress("noreply@example.com", "My App"),\r\n Subject = "Your order has shipped",\r\n Body = "

Your order is on its way!

",\r\n IsBodyHtml = true\r\n};\r\nmessage.To.Add(new MailAddress("customer@example.com", "Jane Doe"));\r\n\r\nawait _emailService.SendEmailAsync(message, ct);\n'})}),"\n",(0,r.jsx)(n.h3,{id:"sending-with-an-attachment",children:"Sending with an attachment"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using var message = new MailMessage("noreply@example.com", "user@example.com")\r\n{\r\n Subject = "Monthly report",\r\n Body = "Please find this month\'s report attached."\r\n};\r\n\r\nawait using var stream = File.OpenRead("/tmp/report.pdf");\r\nmessage.Attachments.Add(new Attachment(stream, "report.pdf", "application/pdf"));\r\n\r\nawait _emailService.SendEmailAsync(message, ct);\n'})}),"\n",(0,r.jsx)(n.h3,{id:"subscribing-to-the-emailsent-event",children:"Subscribing to the EmailSent event"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'_emailService.EmailSent += (sender, args) =>\r\n{\r\n Console.WriteLine($"Email sent to {args.Message.To.First().Address}");\r\n};\n'})}),"\n",(0,r.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,r.jsx)(n.h3,{id:"iemailservice",children:(0,r.jsx)(n.code,{children:"IEmailService"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Member"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SendEmail(MailMessage)"})}),(0,r.jsx)(n.td,{children:"Sends the message synchronously."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SendEmailAsync(MailMessage, CancellationToken)"})}),(0,r.jsx)(n.td,{children:"Sends the message asynchronously."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"EmailSent"})}),(0,r.jsx)(n.td,{children:"Event raised after successful delivery."})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"smtpemailsettings",children:(0,r.jsx)(n.code,{children:"SmtpEmailSettings"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Property"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"Host"})}),(0,r.jsx)(n.td,{children:"SMTP server hostname or IP address."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"Port"})}),(0,r.jsx)(n.td,{children:"SMTP port (commonly 587 for STARTTLS, 465 for SSL, 25 for unencrypted)."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"EnableSsl"})}),(0,r.jsxs)(n.td,{children:["When ",(0,r.jsx)(n.code,{children:"true"}),", the connection is encrypted."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"UserName"})}),(0,r.jsx)(n.td,{children:"SMTP authentication username."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"Password"})}),(0,r.jsx)(n.td,{children:"SMTP authentication password."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"FromEmailDefault"})}),(0,r.jsx)(n.td,{children:"Default sender email address."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"FromNameDefault"})}),(0,r.jsx)(n.td,{children:"Default sender display name."})]})]})]}),"\n",(0,r.jsxs)(n.h3,{id:"ircommonbuilder-extension",children:[(0,r.jsx)(n.code,{children:"IRCommonBuilder"})," extension"]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Method"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsx)(n.tbody,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"WithSmtpEmailServices(Action)"})}),(0,r.jsxs)(n.td,{children:["Registers ",(0,r.jsx)(n.code,{children:"SmtpEmailService"})," as ",(0,r.jsx)(n.code,{children:"IEmailService"})," and configures SMTP settings."]})]})})]})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(m,{...e})}):m(e)}},75783(e,n,i){i.d(n,{A:()=>o});var r=i(30758);const s="container_xjrG",a="label_Y4p8",t="commandRow_FY5I",l="command_m7Qs",d="copyButton_u1GK";var c=i(86070);function o({packageName:e,version:n}){const[i,o]=(0,r.useState)(!1),m=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,c.jsxs)("div",{className:s,children:[(0,c.jsx)("div",{className:a,children:"NuGet Package"}),(0,c.jsxs)("div",{className:t,children:[(0,c.jsx)("code",{className:l,children:m}),(0,c.jsx)("button",{className:d,onClick:()=>{navigator.clipboard.writeText(m),o(!0),setTimeout(()=>o(!1),2e3)},title:"Copy to clipboard",children:i?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,i){i.d(n,{R:()=>t,x:()=>l});var r=i(30758);const s={},a=r.createContext(s);function t(e){const n=r.useContext(a);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:t(e.components),r.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/a283db28.fd3bb2cf.js b/website/build/assets/js/a283db28.fd3bb2cf.js new file mode 100644 index 00000000..6285d880 --- /dev/null +++ b/website/build/assets/js/a283db28.fd3bb2cf.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[5585],{99870(e,n,i){i.r(n),i.d(n,{assets:()=>c,contentTitle:()=>l,default:()=>h,frontMatter:()=>r,metadata:()=>d,toc:()=>o});var s=i(86070),t=i(81753),a=i(75783);const r={title:"Overview",sidebar_position:1,description:"Provider-agnostic email abstraction for .NET with IEmailService, built-in SMTP support via SmtpClient, HTML bodies, attachments, and an EmailSent event."},l="Email Overview",d={id:"email/overview",title:"Overview",description:"Provider-agnostic email abstraction for .NET with IEmailService, built-in SMTP support via SmtpClient, HTML bodies, attachments, and an EmailSent event.",source:"@site/docs/email/overview.mdx",sourceDirName:"email",slug:"/email/overview",permalink:"/docs/next/email/overview",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/email/overview.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Overview",sidebar_position:1,description:"Provider-agnostic email abstraction for .NET with IEmailService, built-in SMTP support via SmtpClient, HTML bodies, attachments, and an EmailSent event."},sidebar:"docsSidebar",previous:{title:"Email",permalink:"/docs/next/category/email"},next:{title:"SendGrid",permalink:"/docs/next/email/sendgrid"}},c={},o=[{value:"Overview",id:"overview",level:2},{value:"Core abstractions",id:"core-abstractions",level:3},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Modular composition",id:"modular-composition",level:3},{value:"Usage",id:"usage",level:2},{value:"Sending a plain-text email",id:"sending-a-plain-text-email",level:3},{value:"Sending an HTML email",id:"sending-an-html-email",level:3},{value:"Sending with an attachment",id:"sending-with-an-attachment",level:3},{value:"Subscribing to the EmailSent event",id:"subscribing-to-the-emailsent-event",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"IEmailService",id:"iemailservice",level:3},{value:"SmtpEmailSettings",id:"smtpemailsettings",level:3},{value:"IRCommonBuilder extension",id:"ircommonbuilder-extension",level:3}];function m(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"email-overview",children:"Email Overview"}),"\n",(0,s.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,s.jsxs)(n.p,{children:["RCommon provides a provider-agnostic email abstraction so that application code does not depend on a specific mail delivery library. Your services depend only on ",(0,s.jsx)(n.code,{children:"IEmailService"}),"; the underlying transport \u2014 SMTP or a cloud API \u2014 is a startup configuration concern."]}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"RCommon.Emailing"})," package delivers a built-in SMTP implementation backed by ",(0,s.jsx)(n.code,{children:"System.Net.Mail.SmtpClient"}),". Third-party providers such as SendGrid are available through separate packages."]}),"\n",(0,s.jsx)(n.h3,{id:"core-abstractions",children:"Core abstractions"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"IEmailService"})," exposes two methods and one event:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"SendEmail(MailMessage)"})," \u2014 synchronous send."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"SendEmailAsync(MailMessage, CancellationToken)"})," \u2014 asynchronous send."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"EmailSent"})," event \u2014 raised after a message is sent successfully, with ",(0,s.jsx)(n.code,{children:"EmailEventArgs"})," carrying the original ",(0,s.jsx)(n.code,{children:"MailMessage"}),"."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Both methods accept a standard ",(0,s.jsx)(n.code,{children:"System.Net.Mail.MailMessage"}),", which means you can use the full .NET mail model (multiple recipients, CC, BCC, attachments, HTML body, inline images) regardless of which provider is configured."]}),"\n",(0,s.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,s.jsx)(a.A,{packageName:"RCommon.Emailing"}),"\n",(0,s.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,s.jsxs)(n.p,{children:["Call ",(0,s.jsx)(n.code,{children:"WithSmtpEmailServices"})," inside your ",(0,s.jsx)(n.code,{children:"AddRCommon()"})," block and provide a delegate that configures ",(0,s.jsx)(n.code,{children:"SmtpEmailSettings"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'using RCommon;\n\nbuilder.Services.AddRCommon()\n .WithSmtpEmailServices(smtp =>\n {\n smtp.Host = "smtp.example.com";\n smtp.Port = 587;\n smtp.EnableSsl = true;\n smtp.UserName = builder.Configuration["Email:UserName"];\n smtp.Password = builder.Configuration["Email:Password"];\n smtp.FromEmailDefault = "noreply@example.com";\n smtp.FromNameDefault = "My Application";\n });\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Bind settings from ",(0,s.jsx)(n.code,{children:"appsettings.json"})," to avoid hard-coding values:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "Email": {\n "Host": "smtp.example.com",\n "Port": 587,\n "EnableSsl": true,\n "UserName": "",\n "Password": "",\n "FromEmailDefault": "noreply@example.com",\n "FromNameDefault": "My Application"\n }\n}\n'})}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'builder.Services.AddRCommon()\n .WithSmtpEmailServices(smtp =>\n builder.Configuration.GetSection("Email").Bind(smtp));\n'})}),"\n",(0,s.jsx)(n.h3,{id:"modular-composition",children:"Modular composition"}),"\n",(0,s.jsxs)(n.p,{children:["Both ",(0,s.jsx)(n.code,{children:"WithSmtpEmailServices"})," and ",(0,s.jsx)(n.code,{children:"WithSendGridEmailServices"})," are singleton-style verbs: only one ",(0,s.jsx)(n.code,{children:"IEmailService"})," implementation makes sense per process, so they enforce the choice at startup."]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Re-registration shape"}),(0,s.jsx)(n.th,{children:"Behaviour"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.td,{children:["Same verb called twice (e.g. ",(0,s.jsx)(n.code,{children:"WithSmtpEmailServices"})," in two modules)"]}),(0,s.jsxs)(n.td,{children:["Idempotent no-op \u2014 the second call detects the impl is already registered and skips the second registration. The latest configuration delegate still runs against ",(0,s.jsx)(n.code,{children:"SmtpEmailSettings"}),", so option values follow last-write-wins."]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.td,{children:["Different email impls in the same process (e.g. one module calls ",(0,s.jsx)(n.code,{children:"WithSmtpEmailServices"}),", another calls ",(0,s.jsx)(n.code,{children:"WithSendGridEmailServices"}),")"]}),(0,s.jsxs)(n.td,{children:["Throws ",(0,s.jsx)(n.code,{children:"RCommonBuilderException"})," naming both impl types. Pick exactly one transport."]})]})]})]}),"\n",(0,s.jsxs)(n.p,{children:["See ",(0,s.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]}),"\n",(0,s.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,s.jsxs)(n.p,{children:["Inject ",(0,s.jsx)(n.code,{children:"IEmailService"})," wherever you need to send mail."]}),"\n",(0,s.jsx)(n.h3,{id:"sending-a-plain-text-email",children:"Sending a plain-text email"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'using System.Net.Mail;\n\npublic class WelcomeEmailService\n{\n private readonly IEmailService _emailService;\n\n public WelcomeEmailService(IEmailService emailService)\n {\n _emailService = emailService;\n }\n\n public async Task SendWelcomeAsync(string recipientEmail,\n string recipientName, CancellationToken ct = default)\n {\n using var message = new MailMessage(\n from: "noreply@example.com",\n to: recipientEmail,\n subject: "Welcome to the platform",\n body: $"Hi {recipientName}, your account is ready.");\n\n await _emailService.SendEmailAsync(message, ct);\n }\n}\n'})}),"\n",(0,s.jsx)(n.h3,{id:"sending-an-html-email",children:"Sending an HTML email"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'using var message = new MailMessage\n{\n From = new MailAddress("noreply@example.com", "My App"),\n Subject = "Your order has shipped",\n Body = "

Your order is on its way!

",\n IsBodyHtml = true\n};\nmessage.To.Add(new MailAddress("customer@example.com", "Jane Doe"));\n\nawait _emailService.SendEmailAsync(message, ct);\n'})}),"\n",(0,s.jsx)(n.h3,{id:"sending-with-an-attachment",children:"Sending with an attachment"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'using var message = new MailMessage("noreply@example.com", "user@example.com")\n{\n Subject = "Monthly report",\n Body = "Please find this month\'s report attached."\n};\n\nawait using var stream = File.OpenRead("/tmp/report.pdf");\nmessage.Attachments.Add(new Attachment(stream, "report.pdf", "application/pdf"));\n\nawait _emailService.SendEmailAsync(message, ct);\n'})}),"\n",(0,s.jsx)(n.h3,{id:"subscribing-to-the-emailsent-event",children:"Subscribing to the EmailSent event"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'_emailService.EmailSent += (sender, args) =>\n{\n Console.WriteLine($"Email sent to {args.Message.To.First().Address}");\n};\n'})}),"\n",(0,s.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,s.jsx)(n.h3,{id:"iemailservice",children:(0,s.jsx)(n.code,{children:"IEmailService"})}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Member"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"SendEmail(MailMessage)"})}),(0,s.jsx)(n.td,{children:"Sends the message synchronously."})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"SendEmailAsync(MailMessage, CancellationToken)"})}),(0,s.jsx)(n.td,{children:"Sends the message asynchronously."})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"EmailSent"})}),(0,s.jsx)(n.td,{children:"Event raised after successful delivery."})]})]})]}),"\n",(0,s.jsx)(n.h3,{id:"smtpemailsettings",children:(0,s.jsx)(n.code,{children:"SmtpEmailSettings"})}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Property"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"Host"})}),(0,s.jsx)(n.td,{children:"SMTP server hostname or IP address."})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"Port"})}),(0,s.jsx)(n.td,{children:"SMTP port (commonly 587 for STARTTLS, 465 for SSL, 25 for unencrypted)."})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"EnableSsl"})}),(0,s.jsxs)(n.td,{children:["When ",(0,s.jsx)(n.code,{children:"true"}),", the connection is encrypted."]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"UserName"})}),(0,s.jsx)(n.td,{children:"SMTP authentication username."})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"Password"})}),(0,s.jsx)(n.td,{children:"SMTP authentication password."})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"FromEmailDefault"})}),(0,s.jsx)(n.td,{children:"Default sender email address."})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"FromNameDefault"})}),(0,s.jsx)(n.td,{children:"Default sender display name."})]})]})]}),"\n",(0,s.jsxs)(n.h3,{id:"ircommonbuilder-extension",children:[(0,s.jsx)(n.code,{children:"IRCommonBuilder"})," extension"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Method"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsx)(n.tbody,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"WithSmtpEmailServices(Action)"})}),(0,s.jsxs)(n.td,{children:["Registers ",(0,s.jsx)(n.code,{children:"SmtpEmailService"})," as ",(0,s.jsx)(n.code,{children:"IEmailService"})," and configures SMTP settings."]})]})})]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(m,{...e})}):m(e)}},75783(e,n,i){i.d(n,{A:()=>o});var s=i(30758);const t="container_xjrG",a="label_Y4p8",r="commandRow_FY5I",l="command_m7Qs",d="copyButton_u1GK";var c=i(86070);function o({packageName:e,version:n}){const[i,o]=(0,s.useState)(!1),m=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,c.jsxs)("div",{className:t,children:[(0,c.jsx)("div",{className:a,children:"NuGet Package"}),(0,c.jsxs)("div",{className:r,children:[(0,c.jsx)("code",{className:l,children:m}),(0,c.jsx)("button",{className:d,onClick:()=>{navigator.clipboard.writeText(m),o(!0),setTimeout(()=>o(!1),2e3)},title:"Copy to clipboard",children:i?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,i){i.d(n,{R:()=>r,x:()=>l});var s=i(30758);const t={},a=s.createContext(t);function r(e){const n=s.useContext(a);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:r(e.components),s.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/af358a8e.9094cab3.js b/website/build/assets/js/af358a8e.9094cab3.js new file mode 100644 index 00000000..20bbea55 --- /dev/null +++ b/website/build/assets/js/af358a8e.9094cab3.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[1262],{60222(e,n,i){i.r(n),i.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>u,frontMatter:()=>r,metadata:()=>a,toc:()=>c});var s=i(86070),o=i(81753),t=i(75783);const r={title:"Newtonsoft.Json",sidebar_position:2,description:"RCommon.JsonNet wraps Newtonsoft.Json behind IJsonSerializer with full JsonSerializerSettings access, custom converters, and camelCase or indented output options."},l="Newtonsoft.Json",a={id:"serialization/newtonsoft",title:"Newtonsoft.Json",description:"RCommon.JsonNet wraps Newtonsoft.Json behind IJsonSerializer with full JsonSerializerSettings access, custom converters, and camelCase or indented output options.",source:"@site/docs/serialization/newtonsoft.mdx",sourceDirName:"serialization",slug:"/serialization/newtonsoft",permalink:"/docs/next/serialization/newtonsoft",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/serialization/newtonsoft.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Newtonsoft.Json",sidebar_position:2,description:"RCommon.JsonNet wraps Newtonsoft.Json behind IJsonSerializer with full JsonSerializerSettings access, custom converters, and camelCase or indented output options."},sidebar:"docsSidebar",previous:{title:"Overview",permalink:"/docs/next/serialization/overview"},next:{title:"System.Text.Json",permalink:"/docs/next/serialization/system-text-json"}},d={},c=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Default setup",id:"default-setup",level:3},{value:"Configuring global serialize options",id:"configuring-global-serialize-options",level:3},{value:"Configuring Newtonsoft.Json settings directly",id:"configuring-newtonsoftjson-settings-directly",level:3},{value:"Modular composition",id:"modular-composition",level:3},{value:"Usage",id:"usage",level:2},{value:"Using a custom Newtonsoft.Json converter",id:"using-a-custom-newtonsoftjson-converter",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"JsonNetBuilder",id:"jsonnetbuilder",level:3},{value:"IJsonNetBuilder extension methods",id:"ijsonnetbuilder-extension-methods",level:3},{value:"JsonNetSerializer",id:"jsonnetserializer",level:3}];function h(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,o.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"newtonsoftjson",children:"Newtonsoft.Json"}),"\n",(0,s.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"RCommon.JsonNet"})," wraps Newtonsoft.Json (Json.NET) behind the ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," interface. It provides the full feature set of Json.NET \u2014 custom converters, reference handling, polymorphic serialization, and so on \u2014 while allowing your application code to depend only on the common abstraction."]}),"\n",(0,s.jsxs)(n.p,{children:["The underlying ",(0,s.jsx)(n.code,{children:"JsonSerializerSettings"})," instance is registered in the DI container via the options pattern, so you can apply any Newtonsoft.Json setting that ",(0,s.jsx)(n.code,{children:"JsonSerializerSettings"})," exposes."]}),"\n",(0,s.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,s.jsx)(t.A,{packageName:"RCommon.JsonNet"}),"\n",(0,s.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,s.jsx)(n.h3,{id:"default-setup",children:"Default setup"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\nusing RCommon.JsonNet;\n\nbuilder.Services.AddRCommon()\n .WithJsonSerialization();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["This registers ",(0,s.jsx)(n.code,{children:"JsonNetSerializer"})," as the ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," implementation with default ",(0,s.jsx)(n.code,{children:"JsonSerializeOptions"})," (camelCase enabled, indentation disabled)."]}),"\n",(0,s.jsx)(n.h3,{id:"configuring-global-serialize-options",children:"Configuring global serialize options"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithJsonSerialization(opts =>\n {\n opts.CamelCase = true;\n opts.Indented = true;\n });\n"})}),"\n",(0,s.jsx)(n.h3,{id:"configuring-newtonsoftjson-settings-directly",children:"Configuring Newtonsoft.Json settings directly"}),"\n",(0,s.jsxs)(n.p,{children:["Use the ",(0,s.jsx)(n.code,{children:"Configure"})," extension on ",(0,s.jsx)(n.code,{children:"IJsonNetBuilder"})," to access the underlying ",(0,s.jsx)(n.code,{children:"JsonSerializerSettings"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"using Newtonsoft.Json;\nusing RCommon;\nusing RCommon.JsonNet;\n\nbuilder.Services.AddRCommon()\n .WithJsonSerialization(b =>\n {\n b.Configure(settings =>\n {\n settings.NullValueHandling = NullValueHandling.Ignore;\n settings.DateFormatHandling = DateFormatHandling.IsoDateFormat;\n settings.Converters.Add(new StringEnumConverter());\n });\n });\n"})}),"\n",(0,s.jsx)(n.p,{children:"Combining global serialize options with custom settings:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithJsonSerialization(\n serializeOptions: opts =>\n {\n opts.CamelCase = true;\n opts.Indented = false;\n },\n deSerializeOptions: _ => { },\n actions: b =>\n {\n b.Configure(settings =>\n {\n settings.NullValueHandling = NullValueHandling.Ignore;\n });\n });\n"})}),"\n",(0,s.jsx)(n.h3,{id:"modular-composition",children:"Modular composition"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"WithJsonSerialization()"})," is a singleton-style verb across all six overloads (parameterless, ",(0,s.jsx)(n.code,{children:"serializeOptions"}),", ",(0,s.jsx)(n.code,{children:"serializeOptions + deSerializeOptions"}),", ",(0,s.jsx)(n.code,{children:"actions"}),", ",(0,s.jsx)(n.code,{children:"serializeOptions + actions"}),", and ",(0,s.jsx)(n.code,{children:"serializeOptions + deSerializeOptions + actions"}),"). Only one ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," implementation makes sense per process, so the verb enforces the choice at startup."]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Re-registration shape"}),(0,s.jsx)(n.th,{children:"Behaviour"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"WithJsonSerialization()"})," called from two modules"]}),(0,s.jsx)(n.td,{children:"Idempotent no-op \u2014 the second registration is skipped, but the configuration delegate still runs so per-call option values follow last-write-wins."})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.td,{children:["One module calls ",(0,s.jsx)(n.code,{children:"WithJsonSerialization()"})," and another calls ",(0,s.jsx)(n.code,{children:"WithJsonSerialization()"})]}),(0,s.jsxs)(n.td,{children:["Throws ",(0,s.jsx)(n.code,{children:"RCommonBuilderException"})," naming both builder types. Pick exactly one JSON serializer."]})]})]})]}),"\n",(0,s.jsxs)(n.p,{children:["See ",(0,s.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]}),"\n",(0,s.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,s.jsxs)(n.p,{children:["Inject ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," and call ",(0,s.jsx)(n.code,{children:"Serialize"})," or ",(0,s.jsx)(n.code,{children:"Deserialize"})," as needed."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public class OrderSyncService\n{\n private readonly IJsonSerializer _serializer;\n\n public OrderSyncService(IJsonSerializer serializer)\n {\n _serializer = serializer;\n }\n\n public string SerializeOrder(Order order)\n {\n return _serializer.Serialize(order);\n }\n\n public Order? DeserializeOrder(string json)\n {\n return _serializer.Deserialize(json);\n }\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"using-a-custom-newtonsoftjson-converter",children:"Using a custom Newtonsoft.Json converter"}),"\n",(0,s.jsxs)(n.p,{children:["Register the converter in settings during startup (see Configuration above), then serialize and deserialize as normal. The converter applies automatically to all calls through ",(0,s.jsx)(n.code,{children:"IJsonSerializer"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"// In startup \u2014 register a custom converter.\nb.Configure(settings =>\n{\n settings.Converters.Add(new MyCustomConverter());\n});\n\n// In application code \u2014 no reference to Newtonsoft.Json needed.\nstring json = _serializer.Serialize(myObject);\nMyType result = _serializer.Deserialize(json)!;\n"})}),"\n",(0,s.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,s.jsx)(n.h3,{id:"jsonnetbuilder",children:(0,s.jsx)(n.code,{children:"JsonNetBuilder"})}),"\n",(0,s.jsxs)(n.p,{children:["Implements ",(0,s.jsx)(n.code,{children:"IJsonBuilder"})," and ",(0,s.jsx)(n.code,{children:"IJsonNetBuilder"}),". Registered automatically by ",(0,s.jsx)(n.code,{children:"WithJsonSerialization"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["On construction it registers ",(0,s.jsx)(n.code,{children:"JsonNetSerializer"})," as ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," with a transient lifetime."]}),"\n",(0,s.jsxs)(n.h3,{id:"ijsonnetbuilder-extension-methods",children:[(0,s.jsx)(n.code,{children:"IJsonNetBuilder"})," extension methods"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Method"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsx)(n.tbody,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"Configure(Action)"})}),(0,s.jsxs)(n.td,{children:["Configures the underlying ",(0,s.jsx)(n.code,{children:"JsonSerializerSettings"})," used by ",(0,s.jsx)(n.code,{children:"JsonNetSerializer"}),". Returns ",(0,s.jsx)(n.code,{children:"IJsonNetBuilder"})," for chaining."]})]})})]}),"\n",(0,s.jsx)(n.h3,{id:"jsonnetserializer",children:(0,s.jsx)(n.code,{children:"JsonNetSerializer"})}),"\n",(0,s.jsxs)(n.p,{children:["Registered as ",(0,s.jsx)(n.code,{children:"IJsonSerializer"}),". Accepts ",(0,s.jsx)(n.code,{children:"IOptions"})," via constructor injection."]}),"\n",(0,s.jsxs)(n.p,{children:["Implements all four ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," methods. Per-call ",(0,s.jsx)(n.code,{children:"JsonSerializeOptions"})," or ",(0,s.jsx)(n.code,{children:"JsonDeserializeOptions"})," override the shared settings instance for that call: ",(0,s.jsx)(n.code,{children:"CamelCase = true"})," applies ",(0,s.jsx)(n.code,{children:"CamelCasePropertyNamesContractResolver"}),"; ",(0,s.jsx)(n.code,{children:"Indented = true"})," sets ",(0,s.jsx)(n.code,{children:"Formatting.Indented"}),"."]})]})}function u(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}},75783(e,n,i){i.d(n,{A:()=>c});var s=i(30758);const o="container_xjrG",t="label_Y4p8",r="commandRow_FY5I",l="command_m7Qs",a="copyButton_u1GK";var d=i(86070);function c({packageName:e,version:n}){const[i,c]=(0,s.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,d.jsxs)("div",{className:o,children:[(0,d.jsx)("div",{className:t,children:"NuGet Package"}),(0,d.jsxs)("div",{className:r,children:[(0,d.jsx)("code",{className:l,children:h}),(0,d.jsx)("button",{className:a,onClick:()=>{navigator.clipboard.writeText(h),c(!0),setTimeout(()=>c(!1),2e3)},title:"Copy to clipboard",children:i?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,i){i.d(n,{R:()=>r,x:()=>l});var s=i(30758);const o={},t=s.createContext(o);function r(e){const n=s.useContext(t);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),s.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/af358a8e.f9424cac.js b/website/build/assets/js/af358a8e.f9424cac.js deleted file mode 100644 index d6b3b10c..00000000 --- a/website/build/assets/js/af358a8e.f9424cac.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[1262],{60222(e,n,i){i.r(n),i.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>h,frontMatter:()=>t,metadata:()=>a,toc:()=>c});var s=i(86070),r=i(81753),o=i(75783);const t={title:"Newtonsoft.Json",sidebar_position:2,description:"RCommon.JsonNet wraps Newtonsoft.Json behind IJsonSerializer with full JsonSerializerSettings access, custom converters, and camelCase or indented output options."},l="Newtonsoft.Json",a={id:"serialization/newtonsoft",title:"Newtonsoft.Json",description:"RCommon.JsonNet wraps Newtonsoft.Json behind IJsonSerializer with full JsonSerializerSettings access, custom converters, and camelCase or indented output options.",source:"@site/docs/serialization/newtonsoft.mdx",sourceDirName:"serialization",slug:"/serialization/newtonsoft",permalink:"/docs/next/serialization/newtonsoft",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/serialization/newtonsoft.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Newtonsoft.Json",sidebar_position:2,description:"RCommon.JsonNet wraps Newtonsoft.Json behind IJsonSerializer with full JsonSerializerSettings access, custom converters, and camelCase or indented output options."},sidebar:"docsSidebar",previous:{title:"Overview",permalink:"/docs/next/serialization/overview"},next:{title:"System.Text.Json",permalink:"/docs/next/serialization/system-text-json"}},d={},c=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Default setup",id:"default-setup",level:3},{value:"Configuring global serialize options",id:"configuring-global-serialize-options",level:3},{value:"Configuring Newtonsoft.Json settings directly",id:"configuring-newtonsoftjson-settings-directly",level:3},{value:"Usage",id:"usage",level:2},{value:"Using a custom Newtonsoft.Json converter",id:"using-a-custom-newtonsoftjson-converter",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"JsonNetBuilder",id:"jsonnetbuilder",level:3},{value:"IJsonNetBuilder extension methods",id:"ijsonnetbuilder-extension-methods",level:3},{value:"JsonNetSerializer",id:"jsonnetserializer",level:3}];function u(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"newtonsoftjson",children:"Newtonsoft.Json"}),"\n",(0,s.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"RCommon.JsonNet"})," wraps Newtonsoft.Json (Json.NET) behind the ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," interface. It provides the full feature set of Json.NET \u2014 custom converters, reference handling, polymorphic serialization, and so on \u2014 while allowing your application code to depend only on the common abstraction."]}),"\n",(0,s.jsxs)(n.p,{children:["The underlying ",(0,s.jsx)(n.code,{children:"JsonSerializerSettings"})," instance is registered in the DI container via the options pattern, so you can apply any Newtonsoft.Json setting that ",(0,s.jsx)(n.code,{children:"JsonSerializerSettings"})," exposes."]}),"\n",(0,s.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,s.jsx)(o.A,{packageName:"RCommon.JsonNet"}),"\n",(0,s.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,s.jsx)(n.h3,{id:"default-setup",children:"Default setup"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\r\nusing RCommon.JsonNet;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithJsonSerialization();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["This registers ",(0,s.jsx)(n.code,{children:"JsonNetSerializer"})," as the ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," implementation with default ",(0,s.jsx)(n.code,{children:"JsonSerializeOptions"})," (camelCase enabled, indentation disabled)."]}),"\n",(0,s.jsx)(n.h3,{id:"configuring-global-serialize-options",children:"Configuring global serialize options"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithJsonSerialization(opts =>\r\n {\r\n opts.CamelCase = true;\r\n opts.Indented = true;\r\n });\n"})}),"\n",(0,s.jsx)(n.h3,{id:"configuring-newtonsoftjson-settings-directly",children:"Configuring Newtonsoft.Json settings directly"}),"\n",(0,s.jsxs)(n.p,{children:["Use the ",(0,s.jsx)(n.code,{children:"Configure"})," extension on ",(0,s.jsx)(n.code,{children:"IJsonNetBuilder"})," to access the underlying ",(0,s.jsx)(n.code,{children:"JsonSerializerSettings"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"using Newtonsoft.Json;\r\nusing RCommon;\r\nusing RCommon.JsonNet;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithJsonSerialization(b =>\r\n {\r\n b.Configure(settings =>\r\n {\r\n settings.NullValueHandling = NullValueHandling.Ignore;\r\n settings.DateFormatHandling = DateFormatHandling.IsoDateFormat;\r\n settings.Converters.Add(new StringEnumConverter());\r\n });\r\n });\n"})}),"\n",(0,s.jsx)(n.p,{children:"Combining global serialize options with custom settings:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithJsonSerialization(\r\n serializeOptions: opts =>\r\n {\r\n opts.CamelCase = true;\r\n opts.Indented = false;\r\n },\r\n deSerializeOptions: _ => { },\r\n actions: b =>\r\n {\r\n b.Configure(settings =>\r\n {\r\n settings.NullValueHandling = NullValueHandling.Ignore;\r\n });\r\n });\n"})}),"\n",(0,s.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,s.jsxs)(n.p,{children:["Inject ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," and call ",(0,s.jsx)(n.code,{children:"Serialize"})," or ",(0,s.jsx)(n.code,{children:"Deserialize"})," as needed."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public class OrderSyncService\r\n{\r\n private readonly IJsonSerializer _serializer;\r\n\r\n public OrderSyncService(IJsonSerializer serializer)\r\n {\r\n _serializer = serializer;\r\n }\r\n\r\n public string SerializeOrder(Order order)\r\n {\r\n return _serializer.Serialize(order);\r\n }\r\n\r\n public Order? DeserializeOrder(string json)\r\n {\r\n return _serializer.Deserialize(json);\r\n }\r\n}\n"})}),"\n",(0,s.jsx)(n.h3,{id:"using-a-custom-newtonsoftjson-converter",children:"Using a custom Newtonsoft.Json converter"}),"\n",(0,s.jsxs)(n.p,{children:["Register the converter in settings during startup (see Configuration above), then serialize and deserialize as normal. The converter applies automatically to all calls through ",(0,s.jsx)(n.code,{children:"IJsonSerializer"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"// In startup \u2014 register a custom converter.\r\nb.Configure(settings =>\r\n{\r\n settings.Converters.Add(new MyCustomConverter());\r\n});\r\n\r\n// In application code \u2014 no reference to Newtonsoft.Json needed.\r\nstring json = _serializer.Serialize(myObject);\r\nMyType result = _serializer.Deserialize(json)!;\n"})}),"\n",(0,s.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,s.jsx)(n.h3,{id:"jsonnetbuilder",children:(0,s.jsx)(n.code,{children:"JsonNetBuilder"})}),"\n",(0,s.jsxs)(n.p,{children:["Implements ",(0,s.jsx)(n.code,{children:"IJsonBuilder"})," and ",(0,s.jsx)(n.code,{children:"IJsonNetBuilder"}),". Registered automatically by ",(0,s.jsx)(n.code,{children:"WithJsonSerialization"}),"."]}),"\n",(0,s.jsxs)(n.p,{children:["On construction it registers ",(0,s.jsx)(n.code,{children:"JsonNetSerializer"})," as ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," with a transient lifetime."]}),"\n",(0,s.jsxs)(n.h3,{id:"ijsonnetbuilder-extension-methods",children:[(0,s.jsx)(n.code,{children:"IJsonNetBuilder"})," extension methods"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Method"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsx)(n.tbody,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"Configure(Action)"})}),(0,s.jsxs)(n.td,{children:["Configures the underlying ",(0,s.jsx)(n.code,{children:"JsonSerializerSettings"})," used by ",(0,s.jsx)(n.code,{children:"JsonNetSerializer"}),". Returns ",(0,s.jsx)(n.code,{children:"IJsonNetBuilder"})," for chaining."]})]})})]}),"\n",(0,s.jsx)(n.h3,{id:"jsonnetserializer",children:(0,s.jsx)(n.code,{children:"JsonNetSerializer"})}),"\n",(0,s.jsxs)(n.p,{children:["Registered as ",(0,s.jsx)(n.code,{children:"IJsonSerializer"}),". Accepts ",(0,s.jsx)(n.code,{children:"IOptions"})," via constructor injection."]}),"\n",(0,s.jsxs)(n.p,{children:["Implements all four ",(0,s.jsx)(n.code,{children:"IJsonSerializer"})," methods. Per-call ",(0,s.jsx)(n.code,{children:"JsonSerializeOptions"})," or ",(0,s.jsx)(n.code,{children:"JsonDeserializeOptions"})," override the shared settings instance for that call: ",(0,s.jsx)(n.code,{children:"CamelCase = true"})," applies ",(0,s.jsx)(n.code,{children:"CamelCasePropertyNamesContractResolver"}),"; ",(0,s.jsx)(n.code,{children:"Indented = true"})," sets ",(0,s.jsx)(n.code,{children:"Formatting.Indented"}),"."]})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(u,{...e})}):u(e)}},75783(e,n,i){i.d(n,{A:()=>c});var s=i(30758);const r="container_xjrG",o="label_Y4p8",t="commandRow_FY5I",l="command_m7Qs",a="copyButton_u1GK";var d=i(86070);function c({packageName:e,version:n}){const[i,c]=(0,s.useState)(!1),u=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,d.jsxs)("div",{className:r,children:[(0,d.jsx)("div",{className:o,children:"NuGet Package"}),(0,d.jsxs)("div",{className:t,children:[(0,d.jsx)("code",{className:l,children:u}),(0,d.jsx)("button",{className:a,onClick:()=>{navigator.clipboard.writeText(u),c(!0),setTimeout(()=>c(!1),2e3)},title:"Copy to clipboard",children:i?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,i){i.d(n,{R:()=>t,x:()=>l});var s=i(30758);const r={},o=s.createContext(r);function t(e){const n=s.useContext(o);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:t(e.components),s.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/afa07236.662fea9d.js b/website/build/assets/js/afa07236.662fea9d.js deleted file mode 100644 index 18fd19e3..00000000 --- a/website/build/assets/js/afa07236.662fea9d.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[641],{32082(e,n,r){r.r(n),r.d(n,{assets:()=>o,contentTitle:()=>d,default:()=>u,frontMatter:()=>a,metadata:()=>c,toc:()=>l});var s=r(86070),i=r(81753),t=r(75783);const a={title:"MassTransit",sidebar_position:4,description:"Configure MassTransit as RCommon's messaging transport, register consumers and producers, choose RabbitMQ or Azure Service Bus, and keep handler code broker-free."},d="MassTransit",c={id:"messaging/masstransit",title:"MassTransit",description:"Configure MassTransit as RCommon's messaging transport, register consumers and producers, choose RabbitMQ or Azure Service Bus, and keep handler code broker-free.",source:"@site/docs/messaging/masstransit.mdx",sourceDirName:"messaging",slug:"/messaging/masstransit",permalink:"/docs/next/messaging/masstransit",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/messaging/masstransit.mdx",tags:[],version:"current",sidebarPosition:4,frontMatter:{title:"MassTransit",sidebar_position:4,description:"Configure MassTransit as RCommon's messaging transport, register consumers and producers, choose RabbitMQ or Azure Service Bus, and keep handler code broker-free."},sidebar:"docsSidebar",previous:{title:"State Machines",permalink:"/docs/next/messaging/state-machines"},next:{title:"Wolverine",permalink:"/docs/next/messaging/wolverine"}},o={},l=[{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Defining an event",id:"defining-an-event",level:2},{value:"Implementing a subscriber (consumer)",id:"implementing-a-subscriber-consumer",level:2},{value:"Producing events",id:"producing-events",level:2},{value:"Publish vs Send",id:"publish-vs-send",level:2},{value:"Transport configuration",id:"transport-configuration",level:2},{value:"Transactional outbox",id:"transactional-outbox",level:2},{value:"How the consumer bridge works",id:"how-the-consumer-bridge-works",level:2},{value:"API summary",id:"api-summary",level:2}];function h(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,i.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"masstransit",children:"MassTransit"}),"\n",(0,s.jsxs)(n.p,{children:["RCommon integrates with MassTransit to deliver events to consumers across service boundaries. The integration registers ",(0,s.jsx)(n.code,{children:"MassTransitEventHandler"})," as a MassTransit ",(0,s.jsx)(n.code,{children:"IConsumer"})," that delegates to the application's ",(0,s.jsx)(n.code,{children:"ISubscriber"}),". Handler code has no dependency on MassTransit types."]}),"\n",(0,s.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,s.jsx)(t.A,{packageName:"RCommon.MassTransit"}),"\n",(0,s.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,s.jsxs)(n.p,{children:["Use ",(0,s.jsx)(n.code,{children:"WithEventHandling"})," on the RCommon builder. The builder inherits from MassTransit's ",(0,s.jsx)(n.code,{children:"ServiceCollectionBusConfigurator"}),", so all standard MassTransit configuration APIs are available directly on it:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'using RCommon;\r\nusing RCommon.MassTransit;\r\nusing RCommon.MassTransit.Producers;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithEventHandling(mt =>\r\n {\r\n // Register the producer that publishes events to the broker\r\n mt.AddProducer();\r\n\r\n // Register subscribers; each call also adds a MassTransit consumer\r\n mt.AddSubscriber();\r\n mt.AddSubscriber();\r\n\r\n // Standard MassTransit transport configuration\r\n mt.UsingRabbitMq((ctx, cfg) =>\r\n {\r\n cfg.Host("rabbitmq://localhost");\r\n cfg.ConfigureEndpoints(ctx);\r\n });\r\n });\n'})}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"AddSubscriber"})," performs three registrations in one call:"]}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["Registers ",(0,s.jsx)(n.code,{children:"ISubscriber"})," in the DI container as transient."]}),"\n",(0,s.jsxs)(n.li,{children:["Calls ",(0,s.jsx)(n.code,{children:"AddConsumer>"})," so MassTransit creates a consumer for the endpoint."]}),"\n",(0,s.jsxs)(n.li,{children:["Records the event-to-producer association in the ",(0,s.jsx)(n.code,{children:"EventSubscriptionManager"})," so the router routes this event only to MassTransit producers."]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"defining-an-event",children:"Defining an event"}),"\n",(0,s.jsxs)(n.p,{children:["Events must implement ",(0,s.jsx)(n.code,{children:"ISerializableEvent"})," and must include a parameterless constructor for broker deserialization:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Models.Events;\r\n\r\npublic class OrderShipped : ISyncEvent\r\n{\r\n public OrderShipped(Guid orderId, string trackingNumber)\r\n {\r\n OrderId = orderId;\r\n TrackingNumber = trackingNumber;\r\n }\r\n\r\n // Required for MassTransit deserialization\r\n public OrderShipped() { }\r\n\r\n public Guid OrderId { get; }\r\n public string TrackingNumber { get; } = string.Empty;\r\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"implementing-a-subscriber-consumer",children:"Implementing a subscriber (consumer)"}),"\n",(0,s.jsxs)(n.p,{children:["Implement ",(0,s.jsx)(n.code,{children:"ISubscriber"}),". No MassTransit types appear in handler code:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'using RCommon.EventHandling.Subscribers;\r\n\r\npublic class OrderShippedHandler : ISubscriber\r\n{\r\n private readonly ILogger _logger;\r\n\r\n public OrderShippedHandler(ILogger logger)\r\n {\r\n _logger = logger;\r\n }\r\n\r\n public async Task HandleAsync(OrderShipped @event, CancellationToken cancellationToken = default)\r\n {\r\n _logger.LogInformation(\r\n "Order {OrderId} shipped with tracking {TrackingNumber}",\r\n @event.OrderId, @event.TrackingNumber);\r\n await Task.CompletedTask;\r\n }\r\n}\n'})}),"\n",(0,s.jsx)(n.h2,{id:"producing-events",children:"Producing events"}),"\n",(0,s.jsxs)(n.p,{children:["Queue events on ",(0,s.jsx)(n.code,{children:"IEventRouter"})," and route them after your database work is complete. The router forwards each event to ",(0,s.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"}),", which calls ",(0,s.jsx)(n.code,{children:"IBus.Publish"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public class ShippingService\r\n{\r\n private readonly IEventRouter _eventRouter;\r\n\r\n public ShippingService(IEventRouter eventRouter)\r\n {\r\n _eventRouter = eventRouter;\r\n }\r\n\r\n public async Task ShipOrderAsync(Guid orderId, string trackingNumber, CancellationToken cancellationToken)\r\n {\r\n // ... perform database work ...\r\n\r\n _eventRouter.AddTransactionalEvent(new OrderShipped(orderId, trackingNumber));\r\n await _eventRouter.RouteEventsAsync(cancellationToken);\r\n }\r\n}\n"})}),"\n",(0,s.jsx)(n.h2,{id:"publish-vs-send",children:"Publish vs Send"}),"\n",(0,s.jsx)(n.p,{children:"Two producers are available. Choose based on the delivery semantics you need:"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Producer"}),(0,s.jsx)(n.th,{children:"MassTransit call"}),(0,s.jsx)(n.th,{children:"Use when"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"IBus.Publish"})}),(0,s.jsx)(n.td,{children:"Fan-out: all consumers subscribed to the type receive the message"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"SendWithMassTransitEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"IBus.Send"})}),(0,s.jsx)(n.td,{children:"Point-to-point: a single endpoint receives the message"})]})]})]}),"\n",(0,s.jsxs)(n.p,{children:["Register ",(0,s.jsx)(n.code,{children:"SendWithMassTransitEventProducer"})," instead of (or alongside) ",(0,s.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"mt.AddProducer();\n"})}),"\n",(0,s.jsx)(n.h2,{id:"transport-configuration",children:"Transport configuration"}),"\n",(0,s.jsx)(n.p,{children:"The builder inherits all MassTransit bus configuration methods. Any transport supported by MassTransit can be used:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'// RabbitMQ\r\nmt.UsingRabbitMq((ctx, cfg) =>\r\n{\r\n cfg.Host("amqp://guest:guest@localhost/");\r\n cfg.ConfigureEndpoints(ctx);\r\n});\r\n\r\n// Azure Service Bus\r\nmt.UsingAzureServiceBus((ctx, cfg) =>\r\n{\r\n cfg.Host("Endpoint=sb://...");\r\n cfg.ConfigureEndpoints(ctx);\r\n});\r\n\r\n// In-memory (for testing)\r\nmt.UsingInMemory((ctx, cfg) =>\r\n{\r\n cfg.ConfigureEndpoints(ctx);\r\n});\n'})}),"\n",(0,s.jsx)(n.h2,{id:"transactional-outbox",children:"Transactional outbox"}),"\n",(0,s.jsxs)(n.p,{children:["Pair MassTransit with the outbox pattern for guaranteed at-least-once delivery. See ",(0,s.jsx)(n.a,{href:"/docs/next/messaging/transactional-outbox",children:"Transactional Outbox"}),"."]}),"\n",(0,s.jsx)(n.h2,{id:"how-the-consumer-bridge-works",children:"How the consumer bridge works"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"MassTransitEventHandler"})," implements both ",(0,s.jsx)(n.code,{children:"IMassTransitEventHandler"})," and MassTransit's ",(0,s.jsx)(n.code,{children:"IConsumer"}),". When MassTransit delivers a message it calls ",(0,s.jsx)(n.code,{children:"Consume(ConsumeContext)"}),", which resolves ",(0,s.jsx)(n.code,{children:"ISubscriber"})," from DI and calls ",(0,s.jsx)(n.code,{children:"HandleAsync"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"// Framework code \u2014 you do not write this yourself\r\npublic class MassTransitEventHandler : IConsumer\r\n where TEvent : class, ISerializableEvent\r\n{\r\n public async Task Consume(ConsumeContext context)\r\n {\r\n await _subscriber.HandleAsync(context.Message, context.CancellationToken);\r\n }\r\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:"This keeps application handler code free of any MassTransit API surface."}),"\n",(0,s.jsx)(n.h2,{id:"api-summary",children:"API summary"}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Type"}),(0,s.jsx)(n.th,{children:"Package"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,s.jsxs)(n.td,{children:["Builder used with ",(0,s.jsx)(n.code,{children:"WithEventHandling"}),"; inherits ",(0,s.jsx)(n.code,{children:"ServiceCollectionBusConfigurator"})]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"IMassTransitEventHandlingBuilder"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,s.jsxs)(n.td,{children:["Interface combining ",(0,s.jsx)(n.code,{children:"IEventHandlingBuilder"})," and ",(0,s.jsx)(n.code,{children:"IBusRegistrationConfigurator"})]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"IEventProducer"})," that calls ",(0,s.jsx)(n.code,{children:"IBus.Publish"})," (fan-out)"]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"SendWithMassTransitEventProducer"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"IEventProducer"})," that calls ",(0,s.jsx)(n.code,{children:"IBus.Send"})," (point-to-point)"]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"MassTransitEventHandler"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,s.jsxs)(n.td,{children:["Internal ",(0,s.jsx)(n.code,{children:"IConsumer"})," that bridges MassTransit to ",(0,s.jsx)(n.code,{children:"ISubscriber"})]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"IMassTransitEventHandler"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,s.jsx)(n.td,{children:"Marker interface for MassTransit event handlers"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"AddSubscriber"})}),(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,s.jsxs)(n.td,{children:["Extension on ",(0,s.jsx)(n.code,{children:"IMassTransitEventHandlingBuilder"}),"; registers handler, consumer, and routing"]})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(h,{...e})}):h(e)}},75783(e,n,r){r.d(n,{A:()=>l});var s=r(30758);const i="container_xjrG",t="label_Y4p8",a="commandRow_FY5I",d="command_m7Qs",c="copyButton_u1GK";var o=r(86070);function l({packageName:e,version:n}){const[r,l]=(0,s.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,o.jsxs)("div",{className:i,children:[(0,o.jsx)("div",{className:t,children:"NuGet Package"}),(0,o.jsxs)("div",{className:a,children:[(0,o.jsx)("code",{className:d,children:h}),(0,o.jsx)("button",{className:c,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:r?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,r){r.d(n,{R:()=>a,x:()=>d});var s=r(30758);const i={},t=s.createContext(i);function a(e){const n=s.useContext(t);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),s.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/afa07236.96328829.js b/website/build/assets/js/afa07236.96328829.js new file mode 100644 index 00000000..9e705bf4 --- /dev/null +++ b/website/build/assets/js/afa07236.96328829.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[641],{32082(e,n,s){s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>d,default:()=>u,frontMatter:()=>a,metadata:()=>o,toc:()=>l});var r=s(86070),i=s(81753),t=s(75783);const a={title:"MassTransit",sidebar_position:4,description:"Configure MassTransit as RCommon's messaging transport, register consumers and producers, choose RabbitMQ or Azure Service Bus, and keep handler code broker-free."},d="MassTransit",o={id:"messaging/masstransit",title:"MassTransit",description:"Configure MassTransit as RCommon's messaging transport, register consumers and producers, choose RabbitMQ or Azure Service Bus, and keep handler code broker-free.",source:"@site/docs/messaging/masstransit.mdx",sourceDirName:"messaging",slug:"/messaging/masstransit",permalink:"/docs/next/messaging/masstransit",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/messaging/masstransit.mdx",tags:[],version:"current",sidebarPosition:4,frontMatter:{title:"MassTransit",sidebar_position:4,description:"Configure MassTransit as RCommon's messaging transport, register consumers and producers, choose RabbitMQ or Azure Service Bus, and keep handler code broker-free."},sidebar:"docsSidebar",previous:{title:"State Machines",permalink:"/docs/next/messaging/state-machines"},next:{title:"Wolverine",permalink:"/docs/next/messaging/wolverine"}},c={},l=[{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Defining an event",id:"defining-an-event",level:2},{value:"Implementing a subscriber (consumer)",id:"implementing-a-subscriber-consumer",level:2},{value:"Producing events",id:"producing-events",level:2},{value:"Publish vs Send",id:"publish-vs-send",level:2},{value:"Transport configuration",id:"transport-configuration",level:2},{value:"Transactional outbox",id:"transactional-outbox",level:2},{value:"How the consumer bridge works",id:"how-the-consumer-bridge-works",level:2},{value:"API summary",id:"api-summary",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"masstransit",children:"MassTransit"}),"\n",(0,r.jsxs)(n.p,{children:["RCommon integrates with MassTransit to deliver events to consumers across service boundaries. The integration registers ",(0,r.jsx)(n.code,{children:"MassTransitEventHandler"})," as a MassTransit ",(0,r.jsx)(n.code,{children:"IConsumer"})," that delegates to the application's ",(0,r.jsx)(n.code,{children:"ISubscriber"}),". Handler code has no dependency on MassTransit types."]}),"\n",(0,r.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(t.A,{packageName:"RCommon.MassTransit"}),"\n",(0,r.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsxs)(n.p,{children:["Use ",(0,r.jsx)(n.code,{children:"WithEventHandling"})," on the RCommon builder. The builder inherits from MassTransit's ",(0,r.jsx)(n.code,{children:"ServiceCollectionBusConfigurator"}),", so all standard MassTransit configuration APIs are available directly on it:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon;\nusing RCommon.MassTransit;\nusing RCommon.MassTransit.Producers;\n\nbuilder.Services.AddRCommon()\n .WithEventHandling(mt =>\n {\n // Register the producer that publishes events to the broker\n mt.AddProducer();\n\n // Register subscribers; each call also adds a MassTransit consumer\n mt.AddSubscriber();\n mt.AddSubscriber();\n\n // Standard MassTransit transport configuration\n mt.UsingRabbitMq((ctx, cfg) =>\n {\n cfg.Host("rabbitmq://localhost");\n cfg.ConfigureEndpoints(ctx);\n });\n });\n'})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"AddSubscriber"})," performs three registrations in one call:"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["Registers ",(0,r.jsx)(n.code,{children:"ISubscriber"})," in the DI container as transient."]}),"\n",(0,r.jsxs)(n.li,{children:["Calls ",(0,r.jsx)(n.code,{children:"AddConsumer>"})," so MassTransit creates a consumer for the endpoint."]}),"\n",(0,r.jsxs)(n.li,{children:["Records the event-to-producer association in the ",(0,r.jsx)(n.code,{children:"EventSubscriptionManager"})," so the router routes this event only to MassTransit producers."]}),"\n"]}),"\n",(0,r.jsxs)(n.admonition,{title:"Modular composition",type:"tip",children:[(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"WithEventHandling"})," participates in the cache-aware sub-builder contract: a second call with the same builder type reuses the cached ",(0,r.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"}),", and subscriber/producer registrations from each module accumulate against the one instance. ",(0,r.jsx)(n.code,{children:"AddProducer"})," deduplicates by concrete producer type, so the same producer registered from two modules yields exactly one descriptor."]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Known limitation \u2014 cross-module composition."})," MassTransit's ",(0,r.jsx)(n.code,{children:"AddMassTransit(...)"})," includes a pre-existing ",(0,r.jsx)(n.code,{children:"IBus"})," guard: invoking it again after the bus has been registered throws ",(0,r.jsx)(n.code,{children:"MassTransit.ConfigurationException"}),". RCommon's bootstrapper caches the sub-builder, but the underlying MassTransit guard runs the first time ",(0,r.jsx)(n.code,{children:"WithEventHandling"})," is called. Single-module usage is unchanged. A second module that calls ",(0,r.jsx)(n.code,{children:"WithEventHandling"})," will throw ",(0,r.jsx)(n.code,{children:"MassTransit.ConfigurationException"}),". Tracked as a follow-up; for now, funnel all MassTransit wiring through one module and let other modules use ",(0,r.jsx)(n.code,{children:"AddSubscriber"})," against the shared cached builder via ",(0,r.jsx)(n.code,{children:"IRCommonBuilder.GetOrAddBuilder()"})," if cross-module subscribers are required."]}),(0,r.jsxs)(n.p,{children:["See ",(0,r.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})]}),"\n",(0,r.jsx)(n.h2,{id:"defining-an-event",children:"Defining an event"}),"\n",(0,r.jsxs)(n.p,{children:["Events must implement ",(0,r.jsx)(n.code,{children:"ISerializableEvent"})," and must include a parameterless constructor for broker deserialization:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Models.Events;\n\npublic class OrderShipped : ISyncEvent\n{\n public OrderShipped(Guid orderId, string trackingNumber)\n {\n OrderId = orderId;\n TrackingNumber = trackingNumber;\n }\n\n // Required for MassTransit deserialization\n public OrderShipped() { }\n\n public Guid OrderId { get; }\n public string TrackingNumber { get; } = string.Empty;\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"implementing-a-subscriber-consumer",children:"Implementing a subscriber (consumer)"}),"\n",(0,r.jsxs)(n.p,{children:["Implement ",(0,r.jsx)(n.code,{children:"ISubscriber"}),". No MassTransit types appear in handler code:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon.EventHandling.Subscribers;\n\npublic class OrderShippedHandler : ISubscriber\n{\n private readonly ILogger _logger;\n\n public OrderShippedHandler(ILogger logger)\n {\n _logger = logger;\n }\n\n public async Task HandleAsync(OrderShipped @event, CancellationToken cancellationToken = default)\n {\n _logger.LogInformation(\n "Order {OrderId} shipped with tracking {TrackingNumber}",\n @event.OrderId, @event.TrackingNumber);\n await Task.CompletedTask;\n }\n}\n'})}),"\n",(0,r.jsx)(n.h2,{id:"producing-events",children:"Producing events"}),"\n",(0,r.jsxs)(n.p,{children:["Queue events on ",(0,r.jsx)(n.code,{children:"IEventRouter"})," and route them after your database work is complete. The router forwards each event to ",(0,r.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"}),", which calls ",(0,r.jsx)(n.code,{children:"IBus.Publish"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"public class ShippingService\n{\n private readonly IEventRouter _eventRouter;\n\n public ShippingService(IEventRouter eventRouter)\n {\n _eventRouter = eventRouter;\n }\n\n public async Task ShipOrderAsync(Guid orderId, string trackingNumber, CancellationToken cancellationToken)\n {\n // ... perform database work ...\n\n _eventRouter.AddTransactionalEvent(new OrderShipped(orderId, trackingNumber));\n await _eventRouter.RouteEventsAsync(cancellationToken);\n }\n}\n"})}),"\n",(0,r.jsx)(n.h2,{id:"publish-vs-send",children:"Publish vs Send"}),"\n",(0,r.jsx)(n.p,{children:"Two producers are available. Choose based on the delivery semantics you need:"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Producer"}),(0,r.jsx)(n.th,{children:"MassTransit call"}),(0,r.jsx)(n.th,{children:"Use when"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"IBus.Publish"})}),(0,r.jsx)(n.td,{children:"Fan-out: all consumers subscribed to the type receive the message"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SendWithMassTransitEventProducer"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"IBus.Send"})}),(0,r.jsx)(n.td,{children:"Point-to-point: a single endpoint receives the message"})]})]})]}),"\n",(0,r.jsxs)(n.p,{children:["Register ",(0,r.jsx)(n.code,{children:"SendWithMassTransitEventProducer"})," instead of (or alongside) ",(0,r.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"mt.AddProducer();\n"})}),"\n",(0,r.jsx)(n.h2,{id:"transport-configuration",children:"Transport configuration"}),"\n",(0,r.jsx)(n.p,{children:"The builder inherits all MassTransit bus configuration methods. Any transport supported by MassTransit can be used:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'// RabbitMQ\nmt.UsingRabbitMq((ctx, cfg) =>\n{\n cfg.Host("amqp://guest:guest@localhost/");\n cfg.ConfigureEndpoints(ctx);\n});\n\n// Azure Service Bus\nmt.UsingAzureServiceBus((ctx, cfg) =>\n{\n cfg.Host("Endpoint=sb://...");\n cfg.ConfigureEndpoints(ctx);\n});\n\n// In-memory (for testing)\nmt.UsingInMemory((ctx, cfg) =>\n{\n cfg.ConfigureEndpoints(ctx);\n});\n'})}),"\n",(0,r.jsx)(n.h2,{id:"transactional-outbox",children:"Transactional outbox"}),"\n",(0,r.jsxs)(n.p,{children:["Pair MassTransit with the outbox pattern for guaranteed at-least-once delivery. See ",(0,r.jsx)(n.a,{href:"/docs/next/messaging/transactional-outbox",children:"Transactional Outbox"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"how-the-consumer-bridge-works",children:"How the consumer bridge works"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"MassTransitEventHandler"})," implements both ",(0,r.jsx)(n.code,{children:"IMassTransitEventHandler"})," and MassTransit's ",(0,r.jsx)(n.code,{children:"IConsumer"}),". When MassTransit delivers a message it calls ",(0,r.jsx)(n.code,{children:"Consume(ConsumeContext)"}),", which resolves ",(0,r.jsx)(n.code,{children:"ISubscriber"})," from DI and calls ",(0,r.jsx)(n.code,{children:"HandleAsync"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:"// Framework code \u2014 you do not write this yourself\npublic class MassTransitEventHandler : IConsumer\n where TEvent : class, ISerializableEvent\n{\n public async Task Consume(ConsumeContext context)\n {\n await _subscriber.HandleAsync(context.Message, context.CancellationToken);\n }\n}\n"})}),"\n",(0,r.jsx)(n.p,{children:"This keeps application handler code free of any MassTransit API surface."}),"\n",(0,r.jsx)(n.h2,{id:"api-summary",children:"API summary"}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Type"}),(0,r.jsx)(n.th,{children:"Package"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"MassTransitEventHandlingBuilder"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,r.jsxs)(n.td,{children:["Builder used with ",(0,r.jsx)(n.code,{children:"WithEventHandling"}),"; inherits ",(0,r.jsx)(n.code,{children:"ServiceCollectionBusConfigurator"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"IMassTransitEventHandlingBuilder"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,r.jsxs)(n.td,{children:["Interface combining ",(0,r.jsx)(n.code,{children:"IEventHandlingBuilder"})," and ",(0,r.jsx)(n.code,{children:"IBusRegistrationConfigurator"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"PublishWithMassTransitEventProducer"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"IEventProducer"})," that calls ",(0,r.jsx)(n.code,{children:"IBus.Publish"})," (fan-out)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SendWithMassTransitEventProducer"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,r.jsxs)(n.td,{children:[(0,r.jsx)(n.code,{children:"IEventProducer"})," that calls ",(0,r.jsx)(n.code,{children:"IBus.Send"})," (point-to-point)"]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"MassTransitEventHandler"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,r.jsxs)(n.td,{children:["Internal ",(0,r.jsx)(n.code,{children:"IConsumer"})," that bridges MassTransit to ",(0,r.jsx)(n.code,{children:"ISubscriber"})]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"IMassTransitEventHandler"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,r.jsx)(n.td,{children:"Marker interface for MassTransit event handlers"})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"AddSubscriber"})}),(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"RCommon.MassTransit"})}),(0,r.jsxs)(n.td,{children:["Extension on ",(0,r.jsx)(n.code,{children:"IMassTransitEventHandlingBuilder"}),"; registers handler, consumer, and routing"]})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},75783(e,n,s){s.d(n,{A:()=>l});var r=s(30758);const i="container_xjrG",t="label_Y4p8",a="commandRow_FY5I",d="command_m7Qs",o="copyButton_u1GK";var c=s(86070);function l({packageName:e,version:n}){const[s,l]=(0,r.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,c.jsxs)("div",{className:i,children:[(0,c.jsx)("div",{className:t,children:"NuGet Package"}),(0,c.jsxs)("div",{className:a,children:[(0,c.jsx)("code",{className:d,children:h}),(0,c.jsx)("button",{className:o,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:s?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,s){s.d(n,{R:()=>a,x:()=>d});var r=s(30758);const i={},t=r.createContext(i);function a(e){const n=r.useContext(t);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),r.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/b1929690.566cba90.js b/website/build/assets/js/b1929690.566cba90.js new file mode 100644 index 00000000..bde9d610 --- /dev/null +++ b/website/build/assets/js/b1929690.566cba90.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[4959],{14087(e,n,i){i.r(n),i.d(n,{assets:()=>l,contentTitle:()=>c,default:()=>h,frontMatter:()=>s,metadata:()=>d,toc:()=>a});var o=i(86070),t=i(81753),r=i(75783);const s={title:"Fluent Configuration",sidebar_position:1,description:"Deep dive into RCommon's fluent configuration API \u2014 IRCommonBuilder, With* extension methods, custom builder extensions, and double-registration protection."},c="Fluent Configuration Builder",d={id:"core-concepts/fluent-configuration",title:"Fluent Configuration",description:"Deep dive into RCommon's fluent configuration API \u2014 IRCommonBuilder, With* extension methods, custom builder extensions, and double-registration protection.",source:"@site/docs/core-concepts/fluent-configuration.mdx",sourceDirName:"core-concepts",slug:"/core-concepts/fluent-configuration",permalink:"/docs/next/core-concepts/fluent-configuration",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/core-concepts/fluent-configuration.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Fluent Configuration",sidebar_position:1,description:"Deep dive into RCommon's fluent configuration API \u2014 IRCommonBuilder, With* extension methods, custom builder extensions, and double-registration protection."},sidebar:"docsSidebar",previous:{title:"Core Concepts",permalink:"/docs/next/category/core-concepts"},next:{title:"Modular Composition",permalink:"/docs/next/core-concepts/modular-composition"}},l={},a=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"What AddRCommon registers automatically",id:"what-addrcommon-registers-automatically",level:3},{value:"Usage",id:"usage",level:2},{value:"Basic setup \u2014 minimal configuration",id:"basic-setup--minimal-configuration",level:3},{value:"Full configuration chain",id:"full-configuration-chain",level:3},{value:"Creating a custom builder extension",id:"creating-a-custom-builder-extension",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"IServiceCollection extension",id:"iservicecollection-extension",level:3},{value:"IRCommonBuilder interface",id:"ircommonbuilder-interface",level:3},{value:"Behavior on repeated calls",id:"behavior-on-repeated-calls",level:3}];function u(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.h1,{id:"fluent-configuration-builder",children:"Fluent Configuration Builder"}),"\n",(0,o.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,o.jsxs)(n.p,{children:["RCommon uses a fluent builder pattern to configure framework services during application startup. The entry point is the ",(0,o.jsx)(n.code,{children:"AddRCommon()"})," extension method on ",(0,o.jsx)(n.code,{children:"IServiceCollection"}),", which returns an ",(0,o.jsx)(n.code,{children:"IRCommonBuilder"})," instance. You then chain ",(0,o.jsx)(n.code,{children:"With*"})," methods on that builder to activate features such as GUID generation, system time, and common factories."]}),"\n",(0,o.jsx)(n.p,{children:"This approach keeps all RCommon configuration in one place, makes dependencies explicit, and prevents double-registration of the same service."}),"\n",(0,o.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,o.jsx)(r.A,{packageName:"RCommon.Core"}),"\n",(0,o.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,o.jsxs)(n.p,{children:["Call ",(0,o.jsx)(n.code,{children:"AddRCommon()"})," inside ",(0,o.jsx)(n.code,{children:"Program.cs"})," (or ",(0,o.jsx)(n.code,{children:"Startup.ConfigureServices"})," for older host models) and chain the features you need:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\n\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddRCommon()\n .WithSequentialGuidGenerator(options =>\n {\n options.DefaultSequentialGuidType = SequentialGuidType.SequentialAtEnd;\n })\n .WithDateTimeSystem(options =>\n {\n options.Kind = DateTimeKind.Utc;\n });\n"})}),"\n",(0,o.jsxs)(n.p,{children:[(0,o.jsx)(n.code,{children:"AddRCommon()"})," registers core infrastructure immediately (caching options, event bus, event router). Each subsequent ",(0,o.jsx)(n.code,{children:"With*"})," call registers the corresponding service into ",(0,o.jsx)(n.code,{children:"IServiceCollection"})," and returns the same builder so calls can be chained."]}),"\n",(0,o.jsx)(n.h3,{id:"what-addrcommon-registers-automatically",children:"What AddRCommon registers automatically"}),"\n",(0,o.jsxs)(n.p,{children:["Calling ",(0,o.jsx)(n.code,{children:"AddRCommon()"})," with no additional configuration registers:"]}),"\n",(0,o.jsxs)(n.ul,{children:["\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"CachingOptions"})," (caching disabled by default)"]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"EventSubscriptionManager"})," (singleton)"]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"IEventBus"})," backed by ",(0,o.jsx)(n.code,{children:"InMemoryEventBus"})," (singleton)"]}),"\n",(0,o.jsxs)(n.li,{children:[(0,o.jsx)(n.code,{children:"IEventRouter"})," backed by ",(0,o.jsx)(n.code,{children:"InMemoryTransactionalEventRouter"})," (scoped)"]}),"\n"]}),"\n",(0,o.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,o.jsx)(n.h3,{id:"basic-setup--minimal-configuration",children:"Basic setup \u2014 minimal configuration"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon();\n"})}),"\n",(0,o.jsx)(n.p,{children:"This is sufficient when you only need the built-in event infrastructure and do not require GUID generation or system time abstractions."}),"\n",(0,o.jsx)(n.h3,{id:"full-configuration-chain",children:"Full configuration chain"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithSimpleGuidGenerator()\n .WithDateTimeSystem(options => options.Kind = DateTimeKind.Utc)\n .WithCommonFactory();\n"})}),"\n",(0,o.jsx)(n.h3,{id:"creating-a-custom-builder-extension",children:"Creating a custom builder extension"}),"\n",(0,o.jsxs)(n.p,{children:["If you are writing an RCommon add-on package you can extend ",(0,o.jsx)(n.code,{children:"IRCommonBuilder"})," with your own ",(0,o.jsx)(n.code,{children:"With*"})," method. Follow the same pattern used by the built-in methods:"]}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-csharp",children:"using Microsoft.Extensions.DependencyInjection;\nusing RCommon;\n\npublic static class MyFeatureBuilderExtensions\n{\n public static IRCommonBuilder WithMyFeature(\n this IRCommonBuilder builder,\n Action configure)\n {\n builder.Services.Configure(configure);\n builder.Services.AddTransient();\n return builder;\n }\n}\n"})}),"\n",(0,o.jsx)(n.p,{children:"Consumers then call it like any other built-in method:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithMyFeature(options => { options.Timeout = TimeSpan.FromSeconds(30); });\n"})}),"\n",(0,o.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,o.jsxs)(n.h3,{id:"iservicecollection-extension",children:[(0,o.jsx)(n.code,{children:"IServiceCollection"})," extension"]}),"\n",(0,o.jsxs)(n.table,{children:[(0,o.jsx)(n.thead,{children:(0,o.jsxs)(n.tr,{children:[(0,o.jsx)(n.th,{children:"Method"}),(0,o.jsx)(n.th,{children:"Description"})]})}),(0,o.jsx)(n.tbody,{children:(0,o.jsxs)(n.tr,{children:[(0,o.jsx)(n.td,{children:(0,o.jsx)(n.code,{children:"AddRCommon()"})}),(0,o.jsxs)(n.td,{children:["Creates an ",(0,o.jsx)(n.code,{children:"RCommonBuilder"}),", registers core services, and returns ",(0,o.jsx)(n.code,{children:"IRCommonBuilder"}),"."]})]})})]}),"\n",(0,o.jsxs)(n.h3,{id:"ircommonbuilder-interface",children:[(0,o.jsx)(n.code,{children:"IRCommonBuilder"})," interface"]}),"\n",(0,o.jsxs)(n.table,{children:[(0,o.jsx)(n.thead,{children:(0,o.jsxs)(n.tr,{children:[(0,o.jsx)(n.th,{children:"Member"}),(0,o.jsx)(n.th,{children:"Description"})]})}),(0,o.jsxs)(n.tbody,{children:[(0,o.jsxs)(n.tr,{children:[(0,o.jsx)(n.td,{children:(0,o.jsx)(n.code,{children:"Services"})}),(0,o.jsxs)(n.td,{children:["The underlying ",(0,o.jsx)(n.code,{children:"IServiceCollection"}),". Useful inside extension methods to register additional services."]})]}),(0,o.jsxs)(n.tr,{children:[(0,o.jsx)(n.td,{children:(0,o.jsx)(n.code,{children:"WithSequentialGuidGenerator(Action)"})}),(0,o.jsxs)(n.td,{children:["Registers ",(0,o.jsx)(n.code,{children:"SequentialGuidGenerator"})," as ",(0,o.jsx)(n.code,{children:"IGuidGenerator"}),"."]})]}),(0,o.jsxs)(n.tr,{children:[(0,o.jsx)(n.td,{children:(0,o.jsx)(n.code,{children:"WithSimpleGuidGenerator()"})}),(0,o.jsxs)(n.td,{children:["Registers ",(0,o.jsx)(n.code,{children:"SimpleGuidGenerator"})," as ",(0,o.jsx)(n.code,{children:"IGuidGenerator"}),"."]})]}),(0,o.jsxs)(n.tr,{children:[(0,o.jsx)(n.td,{children:(0,o.jsx)(n.code,{children:"WithDateTimeSystem(Action)"})}),(0,o.jsxs)(n.td,{children:["Registers ",(0,o.jsx)(n.code,{children:"SystemTime"})," as ",(0,o.jsx)(n.code,{children:"ISystemTime"}),"."]})]}),(0,o.jsxs)(n.tr,{children:[(0,o.jsx)(n.td,{children:(0,o.jsx)(n.code,{children:"WithCommonFactory()"})}),(0,o.jsxs)(n.td,{children:["Registers a service/implementation pair together with an ",(0,o.jsx)(n.code,{children:"ICommonFactory"}),"."]})]}),(0,o.jsxs)(n.tr,{children:[(0,o.jsx)(n.td,{children:(0,o.jsx)(n.code,{children:"Configure()"})}),(0,o.jsxs)(n.td,{children:["Finalizes configuration and returns the ",(0,o.jsx)(n.code,{children:"IServiceCollection"}),". Called internally by ",(0,o.jsx)(n.code,{children:"AddRCommon()"}),"."]})]})]})]}),"\n",(0,o.jsx)(n.h3,{id:"behavior-on-repeated-calls",children:"Behavior on repeated calls"}),"\n",(0,o.jsxs)(n.p,{children:["Calling the same ",(0,o.jsx)(n.code,{children:"With*"})," method twice with the same ",(0,o.jsx)(n.code,{children:"T"})," is now idempotent \u2014 the sub-builder is cached on the underlying ",(0,o.jsx)(n.code,{children:"IServiceCollection"})," and any configuration action delegates accumulate against the cached instance. Singleton-style verbs (e.g., ",(0,o.jsx)(n.code,{children:"WithSimpleGuidGenerator"}),", ",(0,o.jsx)(n.code,{children:"WithDateTimeSystem"}),", ",(0,o.jsx)(n.code,{children:"WithJsonSerialization"}),") are idempotent on same-type re-registration but throw ",(0,o.jsx)(n.code,{children:"RCommonBuilderException"})," on different-type conflicts. See ",(0,o.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix and modular usage patterns."]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(u,{...e})}):u(e)}},75783(e,n,i){i.d(n,{A:()=>a});var o=i(30758);const t="container_xjrG",r="label_Y4p8",s="commandRow_FY5I",c="command_m7Qs",d="copyButton_u1GK";var l=i(86070);function a({packageName:e,version:n}){const[i,a]=(0,o.useState)(!1),u=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,l.jsxs)("div",{className:t,children:[(0,l.jsx)("div",{className:r,children:"NuGet Package"}),(0,l.jsxs)("div",{className:s,children:[(0,l.jsx)("code",{className:c,children:u}),(0,l.jsx)("button",{className:d,onClick:()=>{navigator.clipboard.writeText(u),a(!0),setTimeout(()=>a(!1),2e3)},title:"Copy to clipboard",children:i?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,i){i.d(n,{R:()=>s,x:()=>c});var o=i(30758);const t={},r=o.createContext(t);function s(e){const n=o.useContext(r);return o.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:s(e.components),o.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/b1929690.986a66fe.js b/website/build/assets/js/b1929690.986a66fe.js deleted file mode 100644 index 6d474ecd..00000000 --- a/website/build/assets/js/b1929690.986a66fe.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[4959],{14087(e,n,i){i.r(n),i.d(n,{assets:()=>l,contentTitle:()=>c,default:()=>h,frontMatter:()=>s,metadata:()=>d,toc:()=>a});var t=i(86070),r=i(81753),o=i(75783);const s={title:"Fluent Configuration",sidebar_position:1,description:"Deep dive into RCommon's fluent configuration API \u2014 IRCommonBuilder, With* extension methods, custom builder extensions, and double-registration protection."},c="Fluent Configuration Builder",d={id:"core-concepts/fluent-configuration",title:"Fluent Configuration",description:"Deep dive into RCommon's fluent configuration API \u2014 IRCommonBuilder, With* extension methods, custom builder extensions, and double-registration protection.",source:"@site/docs/core-concepts/fluent-configuration.mdx",sourceDirName:"core-concepts",slug:"/core-concepts/fluent-configuration",permalink:"/docs/next/core-concepts/fluent-configuration",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/core-concepts/fluent-configuration.mdx",tags:[],version:"current",sidebarPosition:1,frontMatter:{title:"Fluent Configuration",sidebar_position:1,description:"Deep dive into RCommon's fluent configuration API \u2014 IRCommonBuilder, With* extension methods, custom builder extensions, and double-registration protection."},sidebar:"docsSidebar",previous:{title:"Core Concepts",permalink:"/docs/next/category/core-concepts"},next:{title:"Guards & Validation",permalink:"/docs/next/core-concepts/guards"}},l={},a=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"What AddRCommon registers automatically",id:"what-addrcommon-registers-automatically",level:3},{value:"Usage",id:"usage",level:2},{value:"Basic setup \u2014 minimal configuration",id:"basic-setup--minimal-configuration",level:3},{value:"Full configuration chain",id:"full-configuration-chain",level:3},{value:"Creating a custom builder extension",id:"creating-a-custom-builder-extension",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"IServiceCollection extension",id:"iservicecollection-extension",level:3},{value:"IRCommonBuilder interface",id:"ircommonbuilder-interface",level:3},{value:"Guard against double registration",id:"guard-against-double-registration",level:3}];function u(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"fluent-configuration-builder",children:"Fluent Configuration Builder"}),"\n",(0,t.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,t.jsxs)(n.p,{children:["RCommon uses a fluent builder pattern to configure framework services during application startup. The entry point is the ",(0,t.jsx)(n.code,{children:"AddRCommon()"})," extension method on ",(0,t.jsx)(n.code,{children:"IServiceCollection"}),", which returns an ",(0,t.jsx)(n.code,{children:"IRCommonBuilder"})," instance. You then chain ",(0,t.jsx)(n.code,{children:"With*"})," methods on that builder to activate features such as GUID generation, system time, and common factories."]}),"\n",(0,t.jsx)(n.p,{children:"This approach keeps all RCommon configuration in one place, makes dependencies explicit, and prevents double-registration of the same service."}),"\n",(0,t.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,t.jsx)(o.A,{packageName:"RCommon.Core"}),"\n",(0,t.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsxs)(n.p,{children:["Call ",(0,t.jsx)(n.code,{children:"AddRCommon()"})," inside ",(0,t.jsx)(n.code,{children:"Program.cs"})," (or ",(0,t.jsx)(n.code,{children:"Startup.ConfigureServices"})," for older host models) and chain the features you need:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\r\n\r\nvar builder = WebApplication.CreateBuilder(args);\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithSequentialGuidGenerator(options =>\r\n {\r\n options.DefaultSequentialGuidType = SequentialGuidType.SequentialAtEnd;\r\n })\r\n .WithDateTimeSystem(options =>\r\n {\r\n options.Kind = DateTimeKind.Utc;\r\n });\n"})}),"\n",(0,t.jsxs)(n.p,{children:[(0,t.jsx)(n.code,{children:"AddRCommon()"})," registers core infrastructure immediately (caching options, event bus, event router). Each subsequent ",(0,t.jsx)(n.code,{children:"With*"})," call registers the corresponding service into ",(0,t.jsx)(n.code,{children:"IServiceCollection"})," and returns the same builder so calls can be chained."]}),"\n",(0,t.jsx)(n.h3,{id:"what-addrcommon-registers-automatically",children:"What AddRCommon registers automatically"}),"\n",(0,t.jsxs)(n.p,{children:["Calling ",(0,t.jsx)(n.code,{children:"AddRCommon()"})," with no additional configuration registers:"]}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"CachingOptions"})," (caching disabled by default)"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"EventSubscriptionManager"})," (singleton)"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"IEventBus"})," backed by ",(0,t.jsx)(n.code,{children:"InMemoryEventBus"})," (singleton)"]}),"\n",(0,t.jsxs)(n.li,{children:[(0,t.jsx)(n.code,{children:"IEventRouter"})," backed by ",(0,t.jsx)(n.code,{children:"InMemoryTransactionalEventRouter"})," (scoped)"]}),"\n"]}),"\n",(0,t.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,t.jsx)(n.h3,{id:"basic-setup--minimal-configuration",children:"Basic setup \u2014 minimal configuration"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon();\n"})}),"\n",(0,t.jsx)(n.p,{children:"This is sufficient when you only need the built-in event infrastructure and do not require GUID generation or system time abstractions."}),"\n",(0,t.jsx)(n.h3,{id:"full-configuration-chain",children:"Full configuration chain"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithSimpleGuidGenerator()\r\n .WithDateTimeSystem(options => options.Kind = DateTimeKind.Utc)\r\n .WithCommonFactory();\n"})}),"\n",(0,t.jsx)(n.h3,{id:"creating-a-custom-builder-extension",children:"Creating a custom builder extension"}),"\n",(0,t.jsxs)(n.p,{children:["If you are writing an RCommon add-on package you can extend ",(0,t.jsx)(n.code,{children:"IRCommonBuilder"})," with your own ",(0,t.jsx)(n.code,{children:"With*"})," method. Follow the same pattern used by the built-in methods:"]}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"using Microsoft.Extensions.DependencyInjection;\r\nusing RCommon;\r\n\r\npublic static class MyFeatureBuilderExtensions\r\n{\r\n public static IRCommonBuilder WithMyFeature(\r\n this IRCommonBuilder builder,\r\n Action configure)\r\n {\r\n builder.Services.Configure(configure);\r\n builder.Services.AddTransient();\r\n return builder;\r\n }\r\n}\n"})}),"\n",(0,t.jsx)(n.p,{children:"Consumers then call it like any other built-in method:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithMyFeature(options => { options.Timeout = TimeSpan.FromSeconds(30); });\n"})}),"\n",(0,t.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,t.jsxs)(n.h3,{id:"iservicecollection-extension",children:[(0,t.jsx)(n.code,{children:"IServiceCollection"})," extension"]}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Method"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsx)(n.tbody,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"AddRCommon()"})}),(0,t.jsxs)(n.td,{children:["Creates an ",(0,t.jsx)(n.code,{children:"RCommonBuilder"}),", registers core services, and returns ",(0,t.jsx)(n.code,{children:"IRCommonBuilder"}),"."]})]})})]}),"\n",(0,t.jsxs)(n.h3,{id:"ircommonbuilder-interface",children:[(0,t.jsx)(n.code,{children:"IRCommonBuilder"})," interface"]}),"\n",(0,t.jsxs)(n.table,{children:[(0,t.jsx)(n.thead,{children:(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.th,{children:"Member"}),(0,t.jsx)(n.th,{children:"Description"})]})}),(0,t.jsxs)(n.tbody,{children:[(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Services"})}),(0,t.jsxs)(n.td,{children:["The underlying ",(0,t.jsx)(n.code,{children:"IServiceCollection"}),". Useful inside extension methods to register additional services."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"WithSequentialGuidGenerator(Action)"})}),(0,t.jsxs)(n.td,{children:["Registers ",(0,t.jsx)(n.code,{children:"SequentialGuidGenerator"})," as ",(0,t.jsx)(n.code,{children:"IGuidGenerator"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"WithSimpleGuidGenerator()"})}),(0,t.jsxs)(n.td,{children:["Registers ",(0,t.jsx)(n.code,{children:"SimpleGuidGenerator"})," as ",(0,t.jsx)(n.code,{children:"IGuidGenerator"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"WithDateTimeSystem(Action)"})}),(0,t.jsxs)(n.td,{children:["Registers ",(0,t.jsx)(n.code,{children:"SystemTime"})," as ",(0,t.jsx)(n.code,{children:"ISystemTime"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"WithCommonFactory()"})}),(0,t.jsxs)(n.td,{children:["Registers a service/implementation pair together with an ",(0,t.jsx)(n.code,{children:"ICommonFactory"}),"."]})]}),(0,t.jsxs)(n.tr,{children:[(0,t.jsx)(n.td,{children:(0,t.jsx)(n.code,{children:"Configure()"})}),(0,t.jsxs)(n.td,{children:["Finalizes configuration and returns the ",(0,t.jsx)(n.code,{children:"IServiceCollection"}),". Called internally by ",(0,t.jsx)(n.code,{children:"AddRCommon()"}),"."]})]})]})]}),"\n",(0,t.jsx)(n.h3,{id:"guard-against-double-registration",children:"Guard against double registration"}),"\n",(0,t.jsxs)(n.p,{children:["The builder tracks whether a GUID generator or date/time system has already been configured. Calling the same ",(0,t.jsx)(n.code,{children:"With*"})," method twice throws an ",(0,t.jsx)(n.code,{children:"RCommonBuilderException"}),", preventing accidental duplicate registrations."]})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(u,{...e})}):u(e)}},75783(e,n,i){i.d(n,{A:()=>a});var t=i(30758);const r="container_xjrG",o="label_Y4p8",s="commandRow_FY5I",c="command_m7Qs",d="copyButton_u1GK";var l=i(86070);function a({packageName:e,version:n}){const[i,a]=(0,t.useState)(!1),u=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,l.jsxs)("div",{className:r,children:[(0,l.jsx)("div",{className:o,children:"NuGet Package"}),(0,l.jsxs)("div",{className:s,children:[(0,l.jsx)("code",{className:c,children:u}),(0,l.jsx)("button",{className:d,onClick:()=>{navigator.clipboard.writeText(u),a(!0),setTimeout(()=>a(!1),2e3)},title:"Copy to clipboard",children:i?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,i){i.d(n,{R:()=>s,x:()=>c});var t=i(30758);const r={},o=t.createContext(r);function s(e){const n=t.useContext(o);return t.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function c(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),t.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/bf1307fc.7b9c846a.js b/website/build/assets/js/bf1307fc.7b9c846a.js new file mode 100644 index 00000000..be3a0a82 --- /dev/null +++ b/website/build/assets/js/bf1307fc.7b9c846a.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[9525],{31413(e){e.exports=JSON.parse('{"version":{"pluginId":"default","version":"current","label":"Next","banner":"unreleased","badge":true,"noIndex":false,"className":"docs-version-current","isLast":false,"docsSidebars":{"docsSidebar":[{"type":"category","label":"Getting Started","items":[{"type":"link","label":"Overview & Philosophy","href":"/docs/next/getting-started/overview","docId":"getting-started/overview","unlisted":false},{"type":"link","label":"Installation","href":"/docs/next/getting-started/installation","docId":"getting-started/installation","unlisted":false},{"type":"link","label":"Quick Start Guide","href":"/docs/next/getting-started/quick-start","docId":"getting-started/quick-start","unlisted":false},{"type":"link","label":"Configuration & Bootstrapping","href":"/docs/next/getting-started/configuration","docId":"getting-started/configuration","unlisted":false},{"type":"link","label":"Dependency Injection","href":"/docs/next/getting-started/dependency-injection","docId":"getting-started/dependency-injection","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/getting-started"},{"type":"category","label":"Core Concepts","items":[{"type":"link","label":"Fluent Configuration","href":"/docs/next/core-concepts/fluent-configuration","docId":"core-concepts/fluent-configuration","unlisted":false},{"type":"link","label":"Modular Composition","href":"/docs/next/core-concepts/modular-composition","docId":"core-concepts/modular-composition","unlisted":false},{"type":"link","label":"Guards & Validation","href":"/docs/next/core-concepts/guards","docId":"core-concepts/guards","unlisted":false},{"type":"link","label":"GUID Generation","href":"/docs/next/core-concepts/guid-generation","docId":"core-concepts/guid-generation","unlisted":false},{"type":"link","label":"System Time Abstraction","href":"/docs/next/core-concepts/system-time","docId":"core-concepts/system-time","unlisted":false},{"type":"link","label":"Execution Results & Models","href":"/docs/next/core-concepts/execution-results","docId":"core-concepts/execution-results","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/core-concepts"},{"type":"category","label":"Domain-Driven Design","items":[{"type":"link","label":"Entities & Aggregate Roots","href":"/docs/next/domain-driven-design/entities-aggregates","docId":"domain-driven-design/entities-aggregates","unlisted":false},{"type":"link","label":"Domain Events","href":"/docs/next/domain-driven-design/domain-events","docId":"domain-driven-design/domain-events","unlisted":false},{"type":"link","label":"Value Objects","href":"/docs/next/domain-driven-design/value-objects","docId":"domain-driven-design/value-objects","unlisted":false},{"type":"link","label":"Auditing","href":"/docs/next/domain-driven-design/auditing","docId":"domain-driven-design/auditing","unlisted":false},{"type":"link","label":"Soft Delete","href":"/docs/next/domain-driven-design/soft-delete","docId":"domain-driven-design/soft-delete","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/domain-driven-design"},{"type":"category","label":"Persistence","items":[{"type":"link","label":"Repository Pattern","href":"/docs/next/persistence/repository-pattern","docId":"persistence/repository-pattern","unlisted":false},{"type":"link","label":"Specifications","href":"/docs/next/persistence/specifications","docId":"persistence/specifications","unlisted":false},{"type":"link","label":"Unit of Work","href":"/docs/next/persistence/unit-of-work","docId":"persistence/unit-of-work","unlisted":false},{"type":"link","label":"Entity Framework Core","href":"/docs/next/persistence/efcore","docId":"persistence/efcore","unlisted":false},{"type":"link","label":"Dapper","href":"/docs/next/persistence/dapper","docId":"persistence/dapper","unlisted":false},{"type":"link","label":"Linq2Db","href":"/docs/next/persistence/linq2db","docId":"persistence/linq2db","unlisted":false},{"type":"link","label":"Sagas","href":"/docs/next/persistence/sagas","docId":"persistence/sagas","unlisted":false},{"type":"link","label":"Caching with Memory","href":"/docs/next/persistence/caching-memory","docId":"persistence/caching-memory","unlisted":false},{"type":"link","label":"Caching with Redis","href":"/docs/next/persistence/caching-redis","docId":"persistence/caching-redis","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/persistence"},{"type":"category","label":"CQRS & Mediator","items":[{"type":"link","label":"Command & Query Bus","href":"/docs/next/cqrs-mediator/command-query-bus","docId":"cqrs-mediator/command-query-bus","unlisted":false},{"type":"link","label":"Commands & Handlers","href":"/docs/next/cqrs-mediator/commands-handlers","docId":"cqrs-mediator/commands-handlers","unlisted":false},{"type":"link","label":"Queries & Handlers","href":"/docs/next/cqrs-mediator/queries-handlers","docId":"cqrs-mediator/queries-handlers","unlisted":false},{"type":"link","label":"MediatR","href":"/docs/next/cqrs-mediator/mediatr","docId":"cqrs-mediator/mediatr","unlisted":false},{"type":"link","label":"Wolverine","href":"/docs/next/cqrs-mediator/wolverine","docId":"cqrs-mediator/wolverine","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/cqrs--mediator"},{"type":"category","label":"Event Handling","items":[{"type":"link","label":"Overview","href":"/docs/next/event-handling/overview","docId":"event-handling/overview","unlisted":false},{"type":"link","label":"In-Memory Events","href":"/docs/next/event-handling/in-memory","docId":"event-handling/in-memory","unlisted":false},{"type":"link","label":"Distributed Events","href":"/docs/next/event-handling/distributed","docId":"event-handling/distributed","unlisted":false},{"type":"link","label":"Transactional Outbox","href":"/docs/next/event-handling/transactional-outbox","docId":"event-handling/transactional-outbox","unlisted":false},{"type":"link","label":"MediatR","href":"/docs/next/event-handling/mediatr","docId":"event-handling/mediatr","unlisted":false},{"type":"link","label":"MassTransit","href":"/docs/next/event-handling/masstransit","docId":"event-handling/masstransit","unlisted":false},{"type":"link","label":"Wolverine","href":"/docs/next/event-handling/wolverine","docId":"event-handling/wolverine","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/event-handling"},{"type":"category","label":"Messaging","items":[{"type":"link","label":"Overview","href":"/docs/next/messaging/overview","docId":"messaging/overview","unlisted":false},{"type":"link","label":"Transactional Outbox","href":"/docs/next/messaging/transactional-outbox","docId":"messaging/transactional-outbox","unlisted":false},{"type":"link","label":"State Machines","href":"/docs/next/messaging/state-machines","docId":"messaging/state-machines","unlisted":false},{"type":"link","label":"MassTransit","href":"/docs/next/messaging/masstransit","docId":"messaging/masstransit","unlisted":false},{"type":"link","label":"Wolverine","href":"/docs/next/messaging/wolverine","docId":"messaging/wolverine","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/messaging"},{"type":"category","label":"State Machines","items":[{"type":"link","label":"Overview","href":"/docs/next/state-machines/overview","docId":"state-machines/overview","unlisted":false},{"type":"link","label":"Stateless","href":"/docs/next/state-machines/stateless","docId":"state-machines/stateless","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/state-machines"},{"type":"category","label":"Caching","items":[{"type":"link","label":"Overview","href":"/docs/next/caching/overview","docId":"caching/overview","unlisted":false},{"type":"link","label":"Memory Cache","href":"/docs/next/caching/memory","docId":"caching/memory","unlisted":false},{"type":"link","label":"Redis Cache","href":"/docs/next/caching/redis","docId":"caching/redis","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/caching"},{"type":"category","label":"Blob Storage","items":[{"type":"link","label":"Overview","href":"/docs/next/blob-storage/overview","docId":"blob-storage/overview","unlisted":false},{"type":"link","label":"Azure Blob Storage","href":"/docs/next/blob-storage/azure","docId":"blob-storage/azure","unlisted":false},{"type":"link","label":"Amazon S3","href":"/docs/next/blob-storage/s3","docId":"blob-storage/s3","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/blob-storage"},{"type":"category","label":"Serialization","items":[{"type":"link","label":"Overview","href":"/docs/next/serialization/overview","docId":"serialization/overview","unlisted":false},{"type":"link","label":"Newtonsoft.Json","href":"/docs/next/serialization/newtonsoft","docId":"serialization/newtonsoft","unlisted":false},{"type":"link","label":"System.Text.Json","href":"/docs/next/serialization/system-text-json","docId":"serialization/system-text-json","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/serialization"},{"type":"category","label":"Validation","items":[{"type":"link","label":"FluentValidation","href":"/docs/next/validation/fluent-validation","docId":"validation/fluent-validation","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/validation"},{"type":"category","label":"Email","items":[{"type":"link","label":"Overview","href":"/docs/next/email/overview","docId":"email/overview","unlisted":false},{"type":"link","label":"SendGrid","href":"/docs/next/email/sendgrid","docId":"email/sendgrid","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/email"},{"type":"category","label":"Multi-Tenancy","items":[{"type":"link","label":"Overview","href":"/docs/next/multi-tenancy/overview","docId":"multi-tenancy/overview","unlisted":false},{"type":"link","label":"Finbuckle","href":"/docs/next/multi-tenancy/finbuckle","docId":"multi-tenancy/finbuckle","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/multi-tenancy"},{"type":"category","label":"Security & Web","items":[{"type":"link","label":"Authorization","href":"/docs/next/security-web/authorization","docId":"security-web/authorization","unlisted":false},{"type":"link","label":"Web Utilities","href":"/docs/next/security-web/web-utilities","docId":"security-web/web-utilities","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/security--web"},{"type":"category","label":"Architecture Guides","items":[{"type":"link","label":"Clean Architecture","href":"/docs/next/architecture-guides/clean-architecture","docId":"architecture-guides/clean-architecture","unlisted":false},{"type":"link","label":"Microservices","href":"/docs/next/architecture-guides/microservices","docId":"architecture-guides/microservices","unlisted":false},{"type":"link","label":"Event-Driven Architecture","href":"/docs/next/architecture-guides/event-driven","docId":"architecture-guides/event-driven","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/architecture-guides"},{"type":"category","label":"Examples & Recipes","items":[{"type":"link","label":"HR Leave Management Sample","href":"/docs/next/examples-recipes/hr-leave-management","docId":"examples-recipes/hr-leave-management","unlisted":false},{"type":"link","label":"Event Handling Examples","href":"/docs/next/examples-recipes/event-handling","docId":"examples-recipes/event-handling","unlisted":false},{"type":"link","label":"Caching Examples","href":"/docs/next/examples-recipes/caching","docId":"examples-recipes/caching","unlisted":false},{"type":"link","label":"Messaging Examples","href":"/docs/next/examples-recipes/messaging","docId":"examples-recipes/messaging","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/examples--recipes"},{"type":"category","label":"Testing","items":[{"type":"link","label":"Overview","href":"/docs/next/testing/overview","docId":"testing/overview","unlisted":false},{"type":"link","label":"Test Base Classes","href":"/docs/next/testing/test-base-classes","docId":"testing/test-base-classes","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/testing"},{"type":"category","label":"API Reference","items":[{"type":"link","label":"NuGet Packages","href":"/docs/next/api-reference/nuget-packages","docId":"api-reference/nuget-packages","unlisted":false},{"type":"link","label":"Changelog","href":"/docs/next/api-reference/changelog","docId":"api-reference/changelog","unlisted":false},{"type":"link","label":"Migration Guide","href":"/docs/next/api-reference/migration-guide","docId":"api-reference/migration-guide","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/api-reference"}]},"docs":{"api-reference/changelog":{"id":"api-reference/changelog","title":"Changelog","description":"RCommon release history covering blob storage abstractions, multi-tenancy support, DDD soft delete, bulk operations, and semantic versioning via MinVer git tags.","sidebar":"docsSidebar"},"api-reference/migration-guide":{"id":"api-reference/migration-guide","title":"Migration Guide","description":"Step-by-step guide for upgrading RCommon between major versions, covering repository interfaces, builder API changes, soft delete, multitenancy, and .NET framework upgrades.","sidebar":"docsSidebar"},"api-reference/nuget-packages":{"id":"api-reference/nuget-packages","title":"NuGet Packages","description":"Complete NuGet package listing for RCommon covering persistence, caching, messaging, mediator, serialization, email, security, multitenancy, and blob storage providers.","sidebar":"docsSidebar"},"architecture-guides/clean-architecture":{"id":"architecture-guides/clean-architecture","title":"Clean Architecture","description":"Build .NET apps with Clean Architecture using RCommon. Layer domain, application, and infrastructure with testable CQRS handlers and swappable persistence providers.","sidebar":"docsSidebar"},"architecture-guides/event-driven":{"id":"architecture-guides/event-driven","title":"Event-Driven Architecture","description":"Implement event-driven .NET apps with RCommon using IEventProducer and ISubscriber across in-memory, MediatR, MassTransit, and Wolverine providers.","sidebar":"docsSidebar"},"architecture-guides/microservices":{"id":"architecture-guides/microservices","title":"Microservices","description":"Build .NET microservices with RCommon using MassTransit or Wolverine for messaging, per-service persistence, and transport-agnostic IEventProducer abstractions.","sidebar":"docsSidebar"},"blob-storage/azure":{"id":"blob-storage/azure","title":"Azure Blob Storage","description":"RCommon.Azure.Blobs wraps the Azure Blob Storage SDK behind IBlobStorageService, supporting connection strings, managed identity, and multiple named stores.","sidebar":"docsSidebar"},"blob-storage/overview":{"id":"blob-storage/overview","title":"Overview","description":"Provider-agnostic blob storage abstraction for .NET covering upload, download, metadata, presigned URLs, and named multi-store registration via IBlobStoreFactory.","sidebar":"docsSidebar"},"blob-storage/s3":{"id":"blob-storage/s3","title":"Amazon S3","description":"RCommon.Amazon.S3Objects wraps the AWS SDK behind IBlobStorageService, supporting explicit credentials, named profiles, IAM instance roles, and S3-compatible stores.","sidebar":"docsSidebar"},"caching/memory":{"id":"caching/memory","title":"Memory Cache","description":"RCommon.MemoryCache provides in-process IMemoryCache and IDistributedCache-backed caching with get-or-create patterns and LINQ expression caching optimization.","sidebar":"docsSidebar"},"caching/overview":{"id":"caching/overview","title":"Overview","description":"Provider-agnostic caching abstraction for .NET with ICacheService, strongly-typed CacheKey, and support for in-memory, distributed, and Redis backends.","sidebar":"docsSidebar"},"caching/redis":{"id":"caching/redis","title":"Redis Cache","description":"RCommon.RedisCache implements ICacheService using StackExchange.Redis for distributed caching across multiple app instances, with JSON serialization support.","sidebar":"docsSidebar"},"core-concepts/execution-results":{"id":"core-concepts/execution-results","title":"Execution Results & Models","description":"Return success or failure using RCommon\'s ExecutionResult pattern, and paginate data with PagedResult and PaginatedListModel \u2014 structured results without exceptions.","sidebar":"docsSidebar"},"core-concepts/fluent-configuration":{"id":"core-concepts/fluent-configuration","title":"Fluent Configuration","description":"Deep dive into RCommon\'s fluent configuration API \u2014 IRCommonBuilder, With* extension methods, custom builder extensions, and double-registration protection.","sidebar":"docsSidebar"},"core-concepts/guards":{"id":"core-concepts/guards","title":"Guards & Validation","description":"Use RCommon Guard clauses for defensive programming \u2014 null checks, string, GUID, numeric, date, collection, type, and custom condition validation in .NET.","sidebar":"docsSidebar"},"core-concepts/guid-generation":{"id":"core-concepts/guid-generation","title":"GUID Generation","description":"RCommon\'s IGuidGenerator abstraction for testable GUID creation \u2014 sequential GUIDs for SQL Server, MySQL, PostgreSQL, and Oracle to reduce index fragmentation.","sidebar":"docsSidebar"},"core-concepts/modular-composition":{"id":"core-concepts/modular-composition","title":"Modular Composition","description":"Compose AddRCommon across multiple modules in a single process \u2014 registrations merge, never duplicate or throw on agreement.","sidebar":"docsSidebar"},"core-concepts/system-time":{"id":"core-concepts/system-time","title":"System Time Abstraction","description":"Use ISystemTime to abstract DateTime.Now for testable time-dependent code \u2014 configure UTC or local time, normalize incoming dates, and substitute clocks in tests.","sidebar":"docsSidebar"},"cqrs-mediator/command-query-bus":{"id":"cqrs-mediator/command-query-bus","title":"Command & Query Bus","description":"Learn how RCommon\'s ICommandBus and IQueryBus implement CQRS, resolve handlers from DI, support validation, and cache compiled dispatch delegates.","sidebar":"docsSidebar"},"cqrs-mediator/commands-handlers":{"id":"cqrs-mediator/commands-handlers","title":"Commands & Handlers","description":"Define ICommand classes, implement ICommandHandler, register handlers individually or by assembly scan, and add FluentValidation before dispatch.","sidebar":"docsSidebar"},"cqrs-mediator/mediatr":{"id":"cqrs-mediator/mediatr","title":"MediatR","description":"Wire MediatR into RCommon\'s mediator abstraction for request-response and fan-out notifications, with built-in logging, validation, and unit-of-work pipeline behaviors.","sidebar":"docsSidebar"},"cqrs-mediator/queries-handlers":{"id":"cqrs-mediator/queries-handlers","title":"Queries & Handlers","description":"Define read-only IQuery classes, implement IQueryHandler, register via assembly scan or explicit registration, and optionally validate queries before dispatch.","sidebar":"docsSidebar"},"cqrs-mediator/wolverine":{"id":"cqrs-mediator/wolverine","title":"Wolverine","description":"Integrate WolverineFx as a distributed event producer in RCommon, supporting fan-out publish and point-to-point send via ISubscriber without Wolverine-specific handler code.","sidebar":"docsSidebar"},"domain-driven-design/auditing":{"id":"domain-driven-design/auditing","title":"Auditing","description":"Capture who created and last modified an entity using RCommon\'s IAuditedEntity interface and AuditedEntity base classes with generic user identifier types.","sidebar":"docsSidebar"},"domain-driven-design/domain-events":{"id":"domain-driven-design/domain-events","title":"Domain Events","description":"Raise, accumulate, and dispatch domain events in RCommon using the DomainEvent base record, ISubscriber handlers, and the post-persistence event routing pipeline.","sidebar":"docsSidebar"},"domain-driven-design/entities-aggregates":{"id":"domain-driven-design/entities-aggregates","title":"Entities & Aggregate Roots","description":"Learn how RCommon\'s BusinessEntity and AggregateRoot base classes give your domain model typed identity, domain event tracking, and optimistic concurrency out of the box.","sidebar":"docsSidebar"},"domain-driven-design/soft-delete":{"id":"domain-driven-design/soft-delete","title":"Soft Delete","description":"Mark records as deleted instead of removing them physically using RCommon\'s ISoftDelete interface, with automatic query filtering and explicit delete mode overrides.","sidebar":"docsSidebar"},"domain-driven-design/value-objects":{"id":"domain-driven-design/value-objects","title":"Value Objects","description":"Model domain concepts by value rather than identity using RCommon\'s ValueObject and ValueObject<T> abstract records with built-in structural equality and immutability.","sidebar":"docsSidebar"},"email/overview":{"id":"email/overview","title":"Overview","description":"Provider-agnostic email abstraction for .NET with IEmailService, built-in SMTP support via SmtpClient, HTML bodies, attachments, and an EmailSent event.","sidebar":"docsSidebar"},"email/sendgrid":{"id":"email/sendgrid","title":"SendGrid","description":"RCommon.SendGrid implements IEmailService via the SendGrid API, converting MailMessage objects to SendGrid calls with attachment and multi-recipient support.","sidebar":"docsSidebar"},"event-handling/distributed":{"id":"event-handling/distributed","title":"Distributed Events","description":"Extend RCommon event handling to message brokers with MassTransit or Wolverine, mixing transports in one app while keeping ISubscriber handler code broker-agnostic.","sidebar":"docsSidebar"},"event-handling/in-memory":{"id":"event-handling/in-memory","title":"In-Memory Events","description":"Configure RCommon\'s in-process event bus with InMemoryEventBusBuilder, implement ISubscriber handlers, and publish events via IEventRouter or IEventBus directly.","sidebar":"docsSidebar"},"event-handling/masstransit":{"id":"event-handling/masstransit","title":"MassTransit","description":"Deliver RCommon events across service boundaries via MassTransit consumers, with fan-out publish or point-to-point send and transport-agnostic ISubscriber handlers.","sidebar":"docsSidebar"},"event-handling/mediatr":{"id":"event-handling/mediatr","title":"MediatR","description":"Route RCommon events through MediatR\'s notification pipeline using MediatREventHandlingBuilder, with ISubscriber handlers unaware of MediatR internals.","sidebar":"docsSidebar"},"event-handling/overview":{"id":"event-handling/overview","title":"Overview","description":"Understand RCommon\'s unified event handling pipeline \u2014 IEventBus, IEventProducer, IEventRouter, and ISubscriber \u2014 and how to choose between in-process and broker transports.","sidebar":"docsSidebar"},"event-handling/transactional-outbox":{"id":"event-handling/transactional-outbox","title":"Transactional Outbox","description":"Guarantee at-least-once event delivery using the transactional outbox pattern with MassTransit EF Core outbox or Wolverine\'s EF Core transaction integration.","sidebar":"docsSidebar"},"event-handling/wolverine":{"id":"event-handling/wolverine","title":"Wolverine","description":"Use Wolverine as an event transport in RCommon\'s event handling pipeline, registering ISubscriber handlers and choosing between fan-out publish or point-to-point send.","sidebar":"docsSidebar"},"examples-recipes/caching":{"id":"examples-recipes/caching","title":"Caching Examples","description":"Practical RCommon caching examples covering IMemoryCache, distributed memory cache, Redis, and ICachingGraphRepository for caching EF Core repository query results.","sidebar":"docsSidebar"},"examples-recipes/event-handling":{"id":"examples-recipes/event-handling","title":"Event Handling Examples","description":"Step-by-step event handling examples for RCommon using InMemoryEventBus and MediatR providers with ISyncEvent, IEventProducer, and ISubscriber patterns.","sidebar":"docsSidebar"},"examples-recipes/hr-leave-management":{"id":"examples-recipes/hr-leave-management","title":"HR Leave Management Sample","description":"Explore RCommon\'s HR Leave Management reference app demonstrating Clean Architecture, CQRS, EF Core, FluentValidation, JWT auth, and SendGrid email integration.","sidebar":"docsSidebar"},"examples-recipes/messaging":{"id":"examples-recipes/messaging","title":"Messaging Examples","description":"Messaging examples for RCommon showing MassTransit and Wolverine transports, subscription isolation between multiple builders, and in-process vs distributed routing.","sidebar":"docsSidebar"},"getting-started/configuration":{"id":"getting-started/configuration","title":"Configuration & Bootstrapping","description":"Configure RCommon with AddRCommon() \u2014 fluent builder methods for GUID generation, system time, persistence, unit of work, mediator, and event handling.","sidebar":"docsSidebar"},"getting-started/dependency-injection":{"id":"getting-started/dependency-injection","title":"Dependency Injection","description":"How RCommon integrates with Microsoft.Extensions.DependencyInjection \u2014 service lifetimes, injecting repositories, IEventBus, ISystemTime, and named data stores.","sidebar":"docsSidebar"},"getting-started/installation":{"id":"getting-started/installation","title":"Installation","description":"Install RCommon NuGet packages for .NET 8, 9, or 10. Pick only what you need \u2014 persistence, CQRS, messaging, caching, validation, blob storage, and more.","sidebar":"docsSidebar"},"getting-started/overview":{"id":"getting-started/overview","title":"Overview & Philosophy","description":"Learn what RCommon provides \u2014 pluggable .NET abstractions for persistence, CQRS, domain events, messaging, and caching with a fluent DI-first configuration API.","sidebar":"docsSidebar"},"getting-started/quick-start":{"id":"getting-started/quick-start","title":"Quick Start Guide","description":"Build a .NET web API with RCommon in minutes \u2014 define entities, configure EF Core persistence, and query via ILinqRepository using a single fluent builder chain.","sidebar":"docsSidebar"},"index":{"id":"index","title":"RCommon Documentation","description":"Complete documentation for RCommon \u2014 a .NET infrastructure library with persistence, CQRS, event handling, messaging, caching, and validation abstractions."},"messaging/masstransit":{"id":"messaging/masstransit","title":"MassTransit","description":"Configure MassTransit as RCommon\'s messaging transport, register consumers and producers, choose RabbitMQ or Azure Service Bus, and keep handler code broker-free.","sidebar":"docsSidebar"},"messaging/overview":{"id":"messaging/overview","title":"Overview","description":"Overview of RCommon\'s broker-backed messaging layer built on IEventRouter and IEventProducer, comparing MassTransit and Wolverine transports for cross-service delivery.","sidebar":"docsSidebar"},"messaging/state-machines":{"id":"messaging/state-machines","title":"State Machines","description":"Orchestrate long-running workflows in RCommon using MassTransit\'s dictionary-based state machine adapter with IStateMachineConfigurator, guards, and async entry/exit actions.","sidebar":"docsSidebar"},"messaging/transactional-outbox":{"id":"messaging/transactional-outbox","title":"Transactional Outbox","description":"Ensure messages reach the broker only when the database transaction commits using the transactional outbox for MassTransit or Wolverine with EF Core integration.","sidebar":"docsSidebar"},"messaging/wolverine":{"id":"messaging/wolverine","title":"Wolverine","description":"Configure Wolverine as RCommon\'s messaging transport with durable delivery, fan-out publish or point-to-point send, and ISubscriber handlers free of Wolverine dependencies.","sidebar":"docsSidebar"},"multi-tenancy/finbuckle":{"id":"multi-tenancy/finbuckle","title":"Finbuckle","description":"RCommon.Finbuckle integrates Finbuckle.MultiTenant with RCommon\'s persistence layer, automatically scoping queries and entity writes to the request-resolved tenant.","sidebar":"docsSidebar"},"multi-tenancy/overview":{"id":"multi-tenancy/overview","title":"Overview","description":"Multi-tenancy abstraction for RCommon that automatically scopes repository queries and entity stamps to the active tenant via ITenantIdAccessor and IMultiTenant.","sidebar":"docsSidebar"},"persistence/caching-memory":{"id":"persistence/caching-memory","title":"Caching with Memory","description":"Decorate RCommon repositories with in-process memory caching using ICachingGraphRepository and caller-supplied cache keys, with write operations always bypassing the cache.","sidebar":"docsSidebar"},"persistence/caching-redis":{"id":"persistence/caching-redis","title":"Caching with Redis","description":"Back RCommon\'s repository caching decorators with Redis via RedisCacheService for a distributed, out-of-process cache shared across multiple service instances and restarts.","sidebar":"docsSidebar"},"persistence/dapper":{"id":"persistence/dapper","title":"Dapper","description":"Use RCommon\'s Dapper provider for lightweight SQL data access with Dommel-generated queries, raw SQL via ISqlMapperRepository, and automatic soft-delete handling.","sidebar":"docsSidebar"},"persistence/efcore":{"id":"persistence/efcore","title":"Entity Framework Core","description":"Configure RCommon\'s EF Core provider to register named DbContexts, inject IGraphRepository with full LINQ support, eager loading, soft delete, and change-tracking control.","sidebar":"docsSidebar"},"persistence/linq2db":{"id":"persistence/linq2db","title":"Linq2Db","description":"Wire up RCommon\'s Linq2Db provider for ILinqRepository support with LINQ queries, paging, eager loading via LoadWith, and multi-database connection management without EF Core.","sidebar":"docsSidebar"},"persistence/repository-pattern":{"id":"persistence/repository-pattern","title":"Repository Pattern","description":"Use RCommon\'s provider-agnostic repository interfaces for async CRUD, paging, eager loading, and soft delete across EF Core, Dapper, and Linq2Db without coupling to a specific ORM.","sidebar":"docsSidebar"},"persistence/sagas":{"id":"persistence/sagas","title":"Sagas","description":"Orchestrate long-running business processes with RCommon\'s saga framework using state machines, durable SagaState persistence, correlation IDs, and automatic compensation.","sidebar":"docsSidebar"},"persistence/specifications":{"id":"persistence/specifications","title":"Specifications","description":"Encapsulate reusable query predicates as named Specification objects, compose them with & and | operators, and pass them directly to any RCommon repository method.","sidebar":"docsSidebar"},"persistence/unit-of-work":{"id":"persistence/unit-of-work","title":"Unit of Work","description":"Coordinate writes across multiple repositories in a single TransactionScope using RCommon\'s IUnitOfWorkFactory, with MediatR and Wolverine pipeline integration support.","sidebar":"docsSidebar"},"security-web/authorization":{"id":"security-web/authorization","title":"Authorization","description":"RCommon.Security provides ICurrentUser and ICurrentPrincipalAccessor for claims-based identity resolution, plus Swashbuckle filters for OpenAPI authorization docs.","sidebar":"docsSidebar"},"security-web/web-utilities":{"id":"security-web/web-utilities","title":"Web Utilities","description":"RCommon.Web bridges ASP.NET Core\'s HttpContext.User to ICurrentPrincipalAccessor, replacing the thread-based default for correct identity resolution in web apps.","sidebar":"docsSidebar"},"serialization/newtonsoft":{"id":"serialization/newtonsoft","title":"Newtonsoft.Json","description":"RCommon.JsonNet wraps Newtonsoft.Json behind IJsonSerializer with full JsonSerializerSettings access, custom converters, and camelCase or indented output options.","sidebar":"docsSidebar"},"serialization/overview":{"id":"serialization/overview","title":"Overview","description":"Provider-agnostic JSON serialization for .NET with IJsonSerializer, supporting Newtonsoft.Json and System.Text.Json with per-call and global option configuration.","sidebar":"docsSidebar"},"serialization/system-text-json":{"id":"serialization/system-text-json","title":"System.Text.Json","description":"RCommon.SystemTextJson wraps System.Text.Json behind IJsonSerializer with full JsonSerializerOptions access and built-in integer and byte enum converters.","sidebar":"docsSidebar"},"state-machines/overview":{"id":"state-machines/overview","title":"Overview","description":"Provider-agnostic finite state machine abstraction for .NET using C# enums, with Stateless and MassTransit adapter options for workflow and lifecycle modeling.","sidebar":"docsSidebar"},"state-machines/stateless":{"id":"state-machines/stateless","title":"Stateless","description":"RCommon.Stateless wraps the Stateless library behind IStateMachineConfigurator, supporting guarded transitions, parameterized triggers, and async entry/exit actions.","sidebar":"docsSidebar"},"testing/overview":{"id":"testing/overview","title":"Overview","description":"Learn how to test .NET applications built on RCommon using mocks, EF Core InMemory, and xUnit or NUnit base classes for unit and integration test strategies.","sidebar":"docsSidebar"},"testing/test-base-classes":{"id":"testing/test-base-classes","title":"Test Base Classes","description":"Reference guide for RCommon.TestBase, TestBootstrapper, TestFixture for xUnit, TestRepository for EF Core integration tests, and TestDataActions fake data generation.","sidebar":"docsSidebar"},"validation/fluent-validation":{"id":"validation/fluent-validation","title":"FluentValidation","description":"RCommon.FluentValidation integrates FluentValidation with the CQRS pipeline for automatic command and query validation, with manual IValidationService support.","sidebar":"docsSidebar"}}}}')}}]); \ No newline at end of file diff --git a/website/build/assets/js/bf1307fc.c8a90dff.js b/website/build/assets/js/bf1307fc.c8a90dff.js deleted file mode 100644 index ddd4cffb..00000000 --- a/website/build/assets/js/bf1307fc.c8a90dff.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[9525],{31413(e){e.exports=JSON.parse('{"version":{"pluginId":"default","version":"current","label":"Next","banner":"unreleased","badge":true,"noIndex":false,"className":"docs-version-current","isLast":false,"docsSidebars":{"docsSidebar":[{"type":"category","label":"Getting Started","items":[{"type":"link","label":"Overview & Philosophy","href":"/docs/next/getting-started/overview","docId":"getting-started/overview","unlisted":false},{"type":"link","label":"Installation","href":"/docs/next/getting-started/installation","docId":"getting-started/installation","unlisted":false},{"type":"link","label":"Quick Start Guide","href":"/docs/next/getting-started/quick-start","docId":"getting-started/quick-start","unlisted":false},{"type":"link","label":"Configuration & Bootstrapping","href":"/docs/next/getting-started/configuration","docId":"getting-started/configuration","unlisted":false},{"type":"link","label":"Dependency Injection","href":"/docs/next/getting-started/dependency-injection","docId":"getting-started/dependency-injection","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/getting-started"},{"type":"category","label":"Core Concepts","items":[{"type":"link","label":"Fluent Configuration","href":"/docs/next/core-concepts/fluent-configuration","docId":"core-concepts/fluent-configuration","unlisted":false},{"type":"link","label":"Guards & Validation","href":"/docs/next/core-concepts/guards","docId":"core-concepts/guards","unlisted":false},{"type":"link","label":"GUID Generation","href":"/docs/next/core-concepts/guid-generation","docId":"core-concepts/guid-generation","unlisted":false},{"type":"link","label":"System Time Abstraction","href":"/docs/next/core-concepts/system-time","docId":"core-concepts/system-time","unlisted":false},{"type":"link","label":"Execution Results & Models","href":"/docs/next/core-concepts/execution-results","docId":"core-concepts/execution-results","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/core-concepts"},{"type":"category","label":"Domain-Driven Design","items":[{"type":"link","label":"Entities & Aggregate Roots","href":"/docs/next/domain-driven-design/entities-aggregates","docId":"domain-driven-design/entities-aggregates","unlisted":false},{"type":"link","label":"Domain Events","href":"/docs/next/domain-driven-design/domain-events","docId":"domain-driven-design/domain-events","unlisted":false},{"type":"link","label":"Value Objects","href":"/docs/next/domain-driven-design/value-objects","docId":"domain-driven-design/value-objects","unlisted":false},{"type":"link","label":"Auditing","href":"/docs/next/domain-driven-design/auditing","docId":"domain-driven-design/auditing","unlisted":false},{"type":"link","label":"Soft Delete","href":"/docs/next/domain-driven-design/soft-delete","docId":"domain-driven-design/soft-delete","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/domain-driven-design"},{"type":"category","label":"Persistence","items":[{"type":"link","label":"Repository Pattern","href":"/docs/next/persistence/repository-pattern","docId":"persistence/repository-pattern","unlisted":false},{"type":"link","label":"Specifications","href":"/docs/next/persistence/specifications","docId":"persistence/specifications","unlisted":false},{"type":"link","label":"Unit of Work","href":"/docs/next/persistence/unit-of-work","docId":"persistence/unit-of-work","unlisted":false},{"type":"link","label":"Entity Framework Core","href":"/docs/next/persistence/efcore","docId":"persistence/efcore","unlisted":false},{"type":"link","label":"Dapper","href":"/docs/next/persistence/dapper","docId":"persistence/dapper","unlisted":false},{"type":"link","label":"Linq2Db","href":"/docs/next/persistence/linq2db","docId":"persistence/linq2db","unlisted":false},{"type":"link","label":"Sagas","href":"/docs/next/persistence/sagas","docId":"persistence/sagas","unlisted":false},{"type":"link","label":"Caching with Memory","href":"/docs/next/persistence/caching-memory","docId":"persistence/caching-memory","unlisted":false},{"type":"link","label":"Caching with Redis","href":"/docs/next/persistence/caching-redis","docId":"persistence/caching-redis","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/persistence"},{"type":"category","label":"CQRS & Mediator","items":[{"type":"link","label":"Command & Query Bus","href":"/docs/next/cqrs-mediator/command-query-bus","docId":"cqrs-mediator/command-query-bus","unlisted":false},{"type":"link","label":"Commands & Handlers","href":"/docs/next/cqrs-mediator/commands-handlers","docId":"cqrs-mediator/commands-handlers","unlisted":false},{"type":"link","label":"Queries & Handlers","href":"/docs/next/cqrs-mediator/queries-handlers","docId":"cqrs-mediator/queries-handlers","unlisted":false},{"type":"link","label":"MediatR","href":"/docs/next/cqrs-mediator/mediatr","docId":"cqrs-mediator/mediatr","unlisted":false},{"type":"link","label":"Wolverine","href":"/docs/next/cqrs-mediator/wolverine","docId":"cqrs-mediator/wolverine","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/cqrs--mediator"},{"type":"category","label":"Event Handling","items":[{"type":"link","label":"Overview","href":"/docs/next/event-handling/overview","docId":"event-handling/overview","unlisted":false},{"type":"link","label":"In-Memory Events","href":"/docs/next/event-handling/in-memory","docId":"event-handling/in-memory","unlisted":false},{"type":"link","label":"Distributed Events","href":"/docs/next/event-handling/distributed","docId":"event-handling/distributed","unlisted":false},{"type":"link","label":"Transactional Outbox","href":"/docs/next/event-handling/transactional-outbox","docId":"event-handling/transactional-outbox","unlisted":false},{"type":"link","label":"MediatR","href":"/docs/next/event-handling/mediatr","docId":"event-handling/mediatr","unlisted":false},{"type":"link","label":"MassTransit","href":"/docs/next/event-handling/masstransit","docId":"event-handling/masstransit","unlisted":false},{"type":"link","label":"Wolverine","href":"/docs/next/event-handling/wolverine","docId":"event-handling/wolverine","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/event-handling"},{"type":"category","label":"Messaging","items":[{"type":"link","label":"Overview","href":"/docs/next/messaging/overview","docId":"messaging/overview","unlisted":false},{"type":"link","label":"Transactional Outbox","href":"/docs/next/messaging/transactional-outbox","docId":"messaging/transactional-outbox","unlisted":false},{"type":"link","label":"State Machines","href":"/docs/next/messaging/state-machines","docId":"messaging/state-machines","unlisted":false},{"type":"link","label":"MassTransit","href":"/docs/next/messaging/masstransit","docId":"messaging/masstransit","unlisted":false},{"type":"link","label":"Wolverine","href":"/docs/next/messaging/wolverine","docId":"messaging/wolverine","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/messaging"},{"type":"category","label":"State Machines","items":[{"type":"link","label":"Overview","href":"/docs/next/state-machines/overview","docId":"state-machines/overview","unlisted":false},{"type":"link","label":"Stateless","href":"/docs/next/state-machines/stateless","docId":"state-machines/stateless","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/state-machines"},{"type":"category","label":"Caching","items":[{"type":"link","label":"Overview","href":"/docs/next/caching/overview","docId":"caching/overview","unlisted":false},{"type":"link","label":"Memory Cache","href":"/docs/next/caching/memory","docId":"caching/memory","unlisted":false},{"type":"link","label":"Redis Cache","href":"/docs/next/caching/redis","docId":"caching/redis","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/caching"},{"type":"category","label":"Blob Storage","items":[{"type":"link","label":"Overview","href":"/docs/next/blob-storage/overview","docId":"blob-storage/overview","unlisted":false},{"type":"link","label":"Azure Blob Storage","href":"/docs/next/blob-storage/azure","docId":"blob-storage/azure","unlisted":false},{"type":"link","label":"Amazon S3","href":"/docs/next/blob-storage/s3","docId":"blob-storage/s3","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/blob-storage"},{"type":"category","label":"Serialization","items":[{"type":"link","label":"Overview","href":"/docs/next/serialization/overview","docId":"serialization/overview","unlisted":false},{"type":"link","label":"Newtonsoft.Json","href":"/docs/next/serialization/newtonsoft","docId":"serialization/newtonsoft","unlisted":false},{"type":"link","label":"System.Text.Json","href":"/docs/next/serialization/system-text-json","docId":"serialization/system-text-json","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/serialization"},{"type":"category","label":"Validation","items":[{"type":"link","label":"FluentValidation","href":"/docs/next/validation/fluent-validation","docId":"validation/fluent-validation","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/validation"},{"type":"category","label":"Email","items":[{"type":"link","label":"Overview","href":"/docs/next/email/overview","docId":"email/overview","unlisted":false},{"type":"link","label":"SendGrid","href":"/docs/next/email/sendgrid","docId":"email/sendgrid","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/email"},{"type":"category","label":"Multi-Tenancy","items":[{"type":"link","label":"Overview","href":"/docs/next/multi-tenancy/overview","docId":"multi-tenancy/overview","unlisted":false},{"type":"link","label":"Finbuckle","href":"/docs/next/multi-tenancy/finbuckle","docId":"multi-tenancy/finbuckle","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/multi-tenancy"},{"type":"category","label":"Security & Web","items":[{"type":"link","label":"Authorization","href":"/docs/next/security-web/authorization","docId":"security-web/authorization","unlisted":false},{"type":"link","label":"Web Utilities","href":"/docs/next/security-web/web-utilities","docId":"security-web/web-utilities","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/security--web"},{"type":"category","label":"Architecture Guides","items":[{"type":"link","label":"Clean Architecture","href":"/docs/next/architecture-guides/clean-architecture","docId":"architecture-guides/clean-architecture","unlisted":false},{"type":"link","label":"Microservices","href":"/docs/next/architecture-guides/microservices","docId":"architecture-guides/microservices","unlisted":false},{"type":"link","label":"Event-Driven Architecture","href":"/docs/next/architecture-guides/event-driven","docId":"architecture-guides/event-driven","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/architecture-guides"},{"type":"category","label":"Examples & Recipes","items":[{"type":"link","label":"HR Leave Management Sample","href":"/docs/next/examples-recipes/hr-leave-management","docId":"examples-recipes/hr-leave-management","unlisted":false},{"type":"link","label":"Event Handling Examples","href":"/docs/next/examples-recipes/event-handling","docId":"examples-recipes/event-handling","unlisted":false},{"type":"link","label":"Caching Examples","href":"/docs/next/examples-recipes/caching","docId":"examples-recipes/caching","unlisted":false},{"type":"link","label":"Messaging Examples","href":"/docs/next/examples-recipes/messaging","docId":"examples-recipes/messaging","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/examples--recipes"},{"type":"category","label":"Testing","items":[{"type":"link","label":"Overview","href":"/docs/next/testing/overview","docId":"testing/overview","unlisted":false},{"type":"link","label":"Test Base Classes","href":"/docs/next/testing/test-base-classes","docId":"testing/test-base-classes","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/testing"},{"type":"category","label":"API Reference","items":[{"type":"link","label":"NuGet Packages","href":"/docs/next/api-reference/nuget-packages","docId":"api-reference/nuget-packages","unlisted":false},{"type":"link","label":"Changelog","href":"/docs/next/api-reference/changelog","docId":"api-reference/changelog","unlisted":false},{"type":"link","label":"Migration Guide","href":"/docs/next/api-reference/migration-guide","docId":"api-reference/migration-guide","unlisted":false}],"collapsed":true,"collapsible":true,"href":"/docs/next/category/api-reference"}]},"docs":{"api-reference/changelog":{"id":"api-reference/changelog","title":"Changelog","description":"RCommon release history covering blob storage abstractions, multi-tenancy support, DDD soft delete, bulk operations, and semantic versioning via MinVer git tags.","sidebar":"docsSidebar"},"api-reference/migration-guide":{"id":"api-reference/migration-guide","title":"Migration Guide","description":"Step-by-step guide for upgrading RCommon between major versions, covering repository interfaces, builder API changes, soft delete, multitenancy, and .NET framework upgrades.","sidebar":"docsSidebar"},"api-reference/nuget-packages":{"id":"api-reference/nuget-packages","title":"NuGet Packages","description":"Complete NuGet package listing for RCommon covering persistence, caching, messaging, mediator, serialization, email, security, multitenancy, and blob storage providers.","sidebar":"docsSidebar"},"architecture-guides/clean-architecture":{"id":"architecture-guides/clean-architecture","title":"Clean Architecture","description":"Build .NET apps with Clean Architecture using RCommon. Layer domain, application, and infrastructure with testable CQRS handlers and swappable persistence providers.","sidebar":"docsSidebar"},"architecture-guides/event-driven":{"id":"architecture-guides/event-driven","title":"Event-Driven Architecture","description":"Implement event-driven .NET apps with RCommon using IEventProducer and ISubscriber across in-memory, MediatR, MassTransit, and Wolverine providers.","sidebar":"docsSidebar"},"architecture-guides/microservices":{"id":"architecture-guides/microservices","title":"Microservices","description":"Build .NET microservices with RCommon using MassTransit or Wolverine for messaging, per-service persistence, and transport-agnostic IEventProducer abstractions.","sidebar":"docsSidebar"},"blob-storage/azure":{"id":"blob-storage/azure","title":"Azure Blob Storage","description":"RCommon.Azure.Blobs wraps the Azure Blob Storage SDK behind IBlobStorageService, supporting connection strings, managed identity, and multiple named stores.","sidebar":"docsSidebar"},"blob-storage/overview":{"id":"blob-storage/overview","title":"Overview","description":"Provider-agnostic blob storage abstraction for .NET covering upload, download, metadata, presigned URLs, and named multi-store registration via IBlobStoreFactory.","sidebar":"docsSidebar"},"blob-storage/s3":{"id":"blob-storage/s3","title":"Amazon S3","description":"RCommon.Amazon.S3Objects wraps the AWS SDK behind IBlobStorageService, supporting explicit credentials, named profiles, IAM instance roles, and S3-compatible stores.","sidebar":"docsSidebar"},"caching/memory":{"id":"caching/memory","title":"Memory Cache","description":"RCommon.MemoryCache provides in-process IMemoryCache and IDistributedCache-backed caching with get-or-create patterns and LINQ expression caching optimization.","sidebar":"docsSidebar"},"caching/overview":{"id":"caching/overview","title":"Overview","description":"Provider-agnostic caching abstraction for .NET with ICacheService, strongly-typed CacheKey, and support for in-memory, distributed, and Redis backends.","sidebar":"docsSidebar"},"caching/redis":{"id":"caching/redis","title":"Redis Cache","description":"RCommon.RedisCache implements ICacheService using StackExchange.Redis for distributed caching across multiple app instances, with JSON serialization support.","sidebar":"docsSidebar"},"core-concepts/execution-results":{"id":"core-concepts/execution-results","title":"Execution Results & Models","description":"Return success or failure using RCommon\'s ExecutionResult pattern, and paginate data with PagedResult and PaginatedListModel \u2014 structured results without exceptions.","sidebar":"docsSidebar"},"core-concepts/fluent-configuration":{"id":"core-concepts/fluent-configuration","title":"Fluent Configuration","description":"Deep dive into RCommon\'s fluent configuration API \u2014 IRCommonBuilder, With* extension methods, custom builder extensions, and double-registration protection.","sidebar":"docsSidebar"},"core-concepts/guards":{"id":"core-concepts/guards","title":"Guards & Validation","description":"Use RCommon Guard clauses for defensive programming \u2014 null checks, string, GUID, numeric, date, collection, type, and custom condition validation in .NET.","sidebar":"docsSidebar"},"core-concepts/guid-generation":{"id":"core-concepts/guid-generation","title":"GUID Generation","description":"RCommon\'s IGuidGenerator abstraction for testable GUID creation \u2014 sequential GUIDs for SQL Server, MySQL, PostgreSQL, and Oracle to reduce index fragmentation.","sidebar":"docsSidebar"},"core-concepts/system-time":{"id":"core-concepts/system-time","title":"System Time Abstraction","description":"Use ISystemTime to abstract DateTime.Now for testable time-dependent code \u2014 configure UTC or local time, normalize incoming dates, and substitute clocks in tests.","sidebar":"docsSidebar"},"cqrs-mediator/command-query-bus":{"id":"cqrs-mediator/command-query-bus","title":"Command & Query Bus","description":"Learn how RCommon\'s ICommandBus and IQueryBus implement CQRS, resolve handlers from DI, support validation, and cache compiled dispatch delegates.","sidebar":"docsSidebar"},"cqrs-mediator/commands-handlers":{"id":"cqrs-mediator/commands-handlers","title":"Commands & Handlers","description":"Define ICommand classes, implement ICommandHandler, register handlers individually or by assembly scan, and add FluentValidation before dispatch.","sidebar":"docsSidebar"},"cqrs-mediator/mediatr":{"id":"cqrs-mediator/mediatr","title":"MediatR","description":"Wire MediatR into RCommon\'s mediator abstraction for request-response and fan-out notifications, with built-in logging, validation, and unit-of-work pipeline behaviors.","sidebar":"docsSidebar"},"cqrs-mediator/queries-handlers":{"id":"cqrs-mediator/queries-handlers","title":"Queries & Handlers","description":"Define read-only IQuery classes, implement IQueryHandler, register via assembly scan or explicit registration, and optionally validate queries before dispatch.","sidebar":"docsSidebar"},"cqrs-mediator/wolverine":{"id":"cqrs-mediator/wolverine","title":"Wolverine","description":"Integrate WolverineFx as a distributed event producer in RCommon, supporting fan-out publish and point-to-point send via ISubscriber without Wolverine-specific handler code.","sidebar":"docsSidebar"},"domain-driven-design/auditing":{"id":"domain-driven-design/auditing","title":"Auditing","description":"Capture who created and last modified an entity using RCommon\'s IAuditedEntity interface and AuditedEntity base classes with generic user identifier types.","sidebar":"docsSidebar"},"domain-driven-design/domain-events":{"id":"domain-driven-design/domain-events","title":"Domain Events","description":"Raise, accumulate, and dispatch domain events in RCommon using the DomainEvent base record, ISubscriber handlers, and the post-persistence event routing pipeline.","sidebar":"docsSidebar"},"domain-driven-design/entities-aggregates":{"id":"domain-driven-design/entities-aggregates","title":"Entities & Aggregate Roots","description":"Learn how RCommon\'s BusinessEntity and AggregateRoot base classes give your domain model typed identity, domain event tracking, and optimistic concurrency out of the box.","sidebar":"docsSidebar"},"domain-driven-design/soft-delete":{"id":"domain-driven-design/soft-delete","title":"Soft Delete","description":"Mark records as deleted instead of removing them physically using RCommon\'s ISoftDelete interface, with automatic query filtering and explicit delete mode overrides.","sidebar":"docsSidebar"},"domain-driven-design/value-objects":{"id":"domain-driven-design/value-objects","title":"Value Objects","description":"Model domain concepts by value rather than identity using RCommon\'s ValueObject and ValueObject<T> abstract records with built-in structural equality and immutability.","sidebar":"docsSidebar"},"email/overview":{"id":"email/overview","title":"Overview","description":"Provider-agnostic email abstraction for .NET with IEmailService, built-in SMTP support via SmtpClient, HTML bodies, attachments, and an EmailSent event.","sidebar":"docsSidebar"},"email/sendgrid":{"id":"email/sendgrid","title":"SendGrid","description":"RCommon.SendGrid implements IEmailService via the SendGrid API, converting MailMessage objects to SendGrid calls with attachment and multi-recipient support.","sidebar":"docsSidebar"},"event-handling/distributed":{"id":"event-handling/distributed","title":"Distributed Events","description":"Extend RCommon event handling to message brokers with MassTransit or Wolverine, mixing transports in one app while keeping ISubscriber handler code broker-agnostic.","sidebar":"docsSidebar"},"event-handling/in-memory":{"id":"event-handling/in-memory","title":"In-Memory Events","description":"Configure RCommon\'s in-process event bus with InMemoryEventBusBuilder, implement ISubscriber handlers, and publish events via IEventRouter or IEventBus directly.","sidebar":"docsSidebar"},"event-handling/masstransit":{"id":"event-handling/masstransit","title":"MassTransit","description":"Deliver RCommon events across service boundaries via MassTransit consumers, with fan-out publish or point-to-point send and transport-agnostic ISubscriber handlers.","sidebar":"docsSidebar"},"event-handling/mediatr":{"id":"event-handling/mediatr","title":"MediatR","description":"Route RCommon events through MediatR\'s notification pipeline using MediatREventHandlingBuilder, with ISubscriber handlers unaware of MediatR internals.","sidebar":"docsSidebar"},"event-handling/overview":{"id":"event-handling/overview","title":"Overview","description":"Understand RCommon\'s unified event handling pipeline \u2014 IEventBus, IEventProducer, IEventRouter, and ISubscriber \u2014 and how to choose between in-process and broker transports.","sidebar":"docsSidebar"},"event-handling/transactional-outbox":{"id":"event-handling/transactional-outbox","title":"Transactional Outbox","description":"Guarantee at-least-once event delivery using the transactional outbox pattern with MassTransit EF Core outbox or Wolverine\'s EF Core transaction integration.","sidebar":"docsSidebar"},"event-handling/wolverine":{"id":"event-handling/wolverine","title":"Wolverine","description":"Use Wolverine as an event transport in RCommon\'s event handling pipeline, registering ISubscriber handlers and choosing between fan-out publish or point-to-point send.","sidebar":"docsSidebar"},"examples-recipes/caching":{"id":"examples-recipes/caching","title":"Caching Examples","description":"Practical RCommon caching examples covering IMemoryCache, distributed memory cache, Redis, and ICachingGraphRepository for caching EF Core repository query results.","sidebar":"docsSidebar"},"examples-recipes/event-handling":{"id":"examples-recipes/event-handling","title":"Event Handling Examples","description":"Step-by-step event handling examples for RCommon using InMemoryEventBus and MediatR providers with ISyncEvent, IEventProducer, and ISubscriber patterns.","sidebar":"docsSidebar"},"examples-recipes/hr-leave-management":{"id":"examples-recipes/hr-leave-management","title":"HR Leave Management Sample","description":"Explore RCommon\'s HR Leave Management reference app demonstrating Clean Architecture, CQRS, EF Core, FluentValidation, JWT auth, and SendGrid email integration.","sidebar":"docsSidebar"},"examples-recipes/messaging":{"id":"examples-recipes/messaging","title":"Messaging Examples","description":"Messaging examples for RCommon showing MassTransit and Wolverine transports, subscription isolation between multiple builders, and in-process vs distributed routing.","sidebar":"docsSidebar"},"getting-started/configuration":{"id":"getting-started/configuration","title":"Configuration & Bootstrapping","description":"Configure RCommon with AddRCommon() \u2014 fluent builder methods for GUID generation, system time, persistence, unit of work, mediator, and event handling.","sidebar":"docsSidebar"},"getting-started/dependency-injection":{"id":"getting-started/dependency-injection","title":"Dependency Injection","description":"How RCommon integrates with Microsoft.Extensions.DependencyInjection \u2014 service lifetimes, injecting repositories, IEventBus, ISystemTime, and named data stores.","sidebar":"docsSidebar"},"getting-started/installation":{"id":"getting-started/installation","title":"Installation","description":"Install RCommon NuGet packages for .NET 8, 9, or 10. Pick only what you need \u2014 persistence, CQRS, messaging, caching, validation, blob storage, and more.","sidebar":"docsSidebar"},"getting-started/overview":{"id":"getting-started/overview","title":"Overview & Philosophy","description":"Learn what RCommon provides \u2014 pluggable .NET abstractions for persistence, CQRS, domain events, messaging, and caching with a fluent DI-first configuration API.","sidebar":"docsSidebar"},"getting-started/quick-start":{"id":"getting-started/quick-start","title":"Quick Start Guide","description":"Build a .NET web API with RCommon in minutes \u2014 define entities, configure EF Core persistence, and query via ILinqRepository using a single fluent builder chain.","sidebar":"docsSidebar"},"index":{"id":"index","title":"RCommon Documentation","description":"Complete documentation for RCommon \u2014 a .NET infrastructure library with persistence, CQRS, event handling, messaging, caching, and validation abstractions."},"messaging/masstransit":{"id":"messaging/masstransit","title":"MassTransit","description":"Configure MassTransit as RCommon\'s messaging transport, register consumers and producers, choose RabbitMQ or Azure Service Bus, and keep handler code broker-free.","sidebar":"docsSidebar"},"messaging/overview":{"id":"messaging/overview","title":"Overview","description":"Overview of RCommon\'s broker-backed messaging layer built on IEventRouter and IEventProducer, comparing MassTransit and Wolverine transports for cross-service delivery.","sidebar":"docsSidebar"},"messaging/state-machines":{"id":"messaging/state-machines","title":"State Machines","description":"Orchestrate long-running workflows in RCommon using MassTransit\'s dictionary-based state machine adapter with IStateMachineConfigurator, guards, and async entry/exit actions.","sidebar":"docsSidebar"},"messaging/transactional-outbox":{"id":"messaging/transactional-outbox","title":"Transactional Outbox","description":"Ensure messages reach the broker only when the database transaction commits using the transactional outbox for MassTransit or Wolverine with EF Core integration.","sidebar":"docsSidebar"},"messaging/wolverine":{"id":"messaging/wolverine","title":"Wolverine","description":"Configure Wolverine as RCommon\'s messaging transport with durable delivery, fan-out publish or point-to-point send, and ISubscriber handlers free of Wolverine dependencies.","sidebar":"docsSidebar"},"multi-tenancy/finbuckle":{"id":"multi-tenancy/finbuckle","title":"Finbuckle","description":"RCommon.Finbuckle integrates Finbuckle.MultiTenant with RCommon\'s persistence layer, automatically scoping queries and entity writes to the request-resolved tenant.","sidebar":"docsSidebar"},"multi-tenancy/overview":{"id":"multi-tenancy/overview","title":"Overview","description":"Multi-tenancy abstraction for RCommon that automatically scopes repository queries and entity stamps to the active tenant via ITenantIdAccessor and IMultiTenant.","sidebar":"docsSidebar"},"persistence/caching-memory":{"id":"persistence/caching-memory","title":"Caching with Memory","description":"Decorate RCommon repositories with in-process memory caching using ICachingGraphRepository and caller-supplied cache keys, with write operations always bypassing the cache.","sidebar":"docsSidebar"},"persistence/caching-redis":{"id":"persistence/caching-redis","title":"Caching with Redis","description":"Back RCommon\'s repository caching decorators with Redis via RedisCacheService for a distributed, out-of-process cache shared across multiple service instances and restarts.","sidebar":"docsSidebar"},"persistence/dapper":{"id":"persistence/dapper","title":"Dapper","description":"Use RCommon\'s Dapper provider for lightweight SQL data access with Dommel-generated queries, raw SQL via ISqlMapperRepository, and automatic soft-delete handling.","sidebar":"docsSidebar"},"persistence/efcore":{"id":"persistence/efcore","title":"Entity Framework Core","description":"Configure RCommon\'s EF Core provider to register named DbContexts, inject IGraphRepository with full LINQ support, eager loading, soft delete, and change-tracking control.","sidebar":"docsSidebar"},"persistence/linq2db":{"id":"persistence/linq2db","title":"Linq2Db","description":"Wire up RCommon\'s Linq2Db provider for ILinqRepository support with LINQ queries, paging, eager loading via LoadWith, and multi-database connection management without EF Core.","sidebar":"docsSidebar"},"persistence/repository-pattern":{"id":"persistence/repository-pattern","title":"Repository Pattern","description":"Use RCommon\'s provider-agnostic repository interfaces for async CRUD, paging, eager loading, and soft delete across EF Core, Dapper, and Linq2Db without coupling to a specific ORM.","sidebar":"docsSidebar"},"persistence/sagas":{"id":"persistence/sagas","title":"Sagas","description":"Orchestrate long-running business processes with RCommon\'s saga framework using state machines, durable SagaState persistence, correlation IDs, and automatic compensation.","sidebar":"docsSidebar"},"persistence/specifications":{"id":"persistence/specifications","title":"Specifications","description":"Encapsulate reusable query predicates as named Specification objects, compose them with & and | operators, and pass them directly to any RCommon repository method.","sidebar":"docsSidebar"},"persistence/unit-of-work":{"id":"persistence/unit-of-work","title":"Unit of Work","description":"Coordinate writes across multiple repositories in a single TransactionScope using RCommon\'s IUnitOfWorkFactory, with MediatR and Wolverine pipeline integration support.","sidebar":"docsSidebar"},"security-web/authorization":{"id":"security-web/authorization","title":"Authorization","description":"RCommon.Security provides ICurrentUser and ICurrentPrincipalAccessor for claims-based identity resolution, plus Swashbuckle filters for OpenAPI authorization docs.","sidebar":"docsSidebar"},"security-web/web-utilities":{"id":"security-web/web-utilities","title":"Web Utilities","description":"RCommon.Web bridges ASP.NET Core\'s HttpContext.User to ICurrentPrincipalAccessor, replacing the thread-based default for correct identity resolution in web apps.","sidebar":"docsSidebar"},"serialization/newtonsoft":{"id":"serialization/newtonsoft","title":"Newtonsoft.Json","description":"RCommon.JsonNet wraps Newtonsoft.Json behind IJsonSerializer with full JsonSerializerSettings access, custom converters, and camelCase or indented output options.","sidebar":"docsSidebar"},"serialization/overview":{"id":"serialization/overview","title":"Overview","description":"Provider-agnostic JSON serialization for .NET with IJsonSerializer, supporting Newtonsoft.Json and System.Text.Json with per-call and global option configuration.","sidebar":"docsSidebar"},"serialization/system-text-json":{"id":"serialization/system-text-json","title":"System.Text.Json","description":"RCommon.SystemTextJson wraps System.Text.Json behind IJsonSerializer with full JsonSerializerOptions access and built-in integer and byte enum converters.","sidebar":"docsSidebar"},"state-machines/overview":{"id":"state-machines/overview","title":"Overview","description":"Provider-agnostic finite state machine abstraction for .NET using C# enums, with Stateless and MassTransit adapter options for workflow and lifecycle modeling.","sidebar":"docsSidebar"},"state-machines/stateless":{"id":"state-machines/stateless","title":"Stateless","description":"RCommon.Stateless wraps the Stateless library behind IStateMachineConfigurator, supporting guarded transitions, parameterized triggers, and async entry/exit actions.","sidebar":"docsSidebar"},"testing/overview":{"id":"testing/overview","title":"Overview","description":"Learn how to test .NET applications built on RCommon using mocks, EF Core InMemory, and xUnit or NUnit base classes for unit and integration test strategies.","sidebar":"docsSidebar"},"testing/test-base-classes":{"id":"testing/test-base-classes","title":"Test Base Classes","description":"Reference guide for RCommon.TestBase, TestBootstrapper, TestFixture for xUnit, TestRepository for EF Core integration tests, and TestDataActions fake data generation.","sidebar":"docsSidebar"},"validation/fluent-validation":{"id":"validation/fluent-validation","title":"FluentValidation","description":"RCommon.FluentValidation integrates FluentValidation with the CQRS pipeline for automatic command and query validation, with manual IValidationService support.","sidebar":"docsSidebar"}}}}')}}]); \ No newline at end of file diff --git a/website/build/assets/js/d7ba9c2b.3e849bca.js b/website/build/assets/js/d7ba9c2b.3e849bca.js new file mode 100644 index 00000000..741683a2 --- /dev/null +++ b/website/build/assets/js/d7ba9c2b.3e849bca.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[1475],{95550(e,n,r){r.r(n),r.d(n,{assets:()=>l,contentTitle:()=>d,default:()=>x,frontMatter:()=>c,metadata:()=>o,toc:()=>a});var i=r(86070),t=r(81753),s=r(75783);const c={title:"Guards & Validation",sidebar_position:2,description:"Use RCommon Guard clauses for defensive programming \u2014 null checks, string, GUID, numeric, date, collection, type, and custom condition validation in .NET."},d="Guards & Validation",o={id:"core-concepts/guards",title:"Guards & Validation",description:"Use RCommon Guard clauses for defensive programming \u2014 null checks, string, GUID, numeric, date, collection, type, and custom condition validation in .NET.",source:"@site/docs/core-concepts/guards.mdx",sourceDirName:"core-concepts",slug:"/core-concepts/guards",permalink:"/docs/next/core-concepts/guards",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/core-concepts/guards.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Guards & Validation",sidebar_position:2,description:"Use RCommon Guard clauses for defensive programming \u2014 null checks, string, GUID, numeric, date, collection, type, and custom condition validation in .NET."},sidebar:"docsSidebar",previous:{title:"Modular Composition",permalink:"/docs/next/core-concepts/modular-composition"},next:{title:"GUID Generation",permalink:"/docs/next/core-concepts/guid-generation"}},l={},a=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Usage",id:"usage",level:2},{value:"Null checks",id:"null-checks",level:3},{value:"String checks",id:"string-checks",level:3},{value:"GUID checks",id:"guid-checks",level:3},{value:"Numeric checks",id:"numeric-checks",level:3},{value:"Date checks",id:"date-checks",level:3},{value:"Collection checks",id:"collection-checks",level:3},{value:"Arbitrary condition checks",id:"arbitrary-condition-checks",level:3},{value:"Type and interface checks",id:"type-and-interface-checks",level:3},{value:"Equality checks",id:"equality-checks",level:3},{value:"API Summary",id:"api-summary",level:2}];function h(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"guards--validation",children:"Guards & Validation"}),"\n",(0,i.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsxs)(n.p,{children:["RCommon ships a ",(0,i.jsx)(n.code,{children:"Guard"})," utility class that implements the guard clause pattern. Guard clauses validate inputs at the top of a method and throw a descriptive exception immediately when a precondition is violated, rather than allowing invalid state to propagate further into the call stack."]}),"\n",(0,i.jsx)(n.p,{children:"All methods are static, so no instantiation or dependency injection is required. You call them directly at the start of any method that needs to validate its arguments."}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(s.A,{packageName:"RCommon.Core"}),"\n",(0,i.jsxs)(n.p,{children:["No additional configuration is required. The ",(0,i.jsx)(n.code,{children:"Guard"})," class is part of the ",(0,i.jsx)(n.code,{children:"RCommon"})," namespace and is available as soon as the package is referenced."]}),"\n",(0,i.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(n.h3,{id:"null-checks",children:"Null checks"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\r\n\r\npublic class OrderService\r\n{\r\n private readonly IOrderRepository _repository;\r\n\r\n public OrderService(IOrderRepository repository)\r\n {\r\n Guard.IsNotNull(repository, nameof(repository));\r\n _repository = repository;\r\n }\r\n\r\n public async Task SubmitOrder(Order order)\r\n {\r\n Guard.IsNotNull(order, nameof(order));\r\n await _repository.SaveAsync(order);\r\n }\r\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"Guard.IsNotNull"})," throws ",(0,i.jsx)(n.code,{children:"ArgumentNullException"})," when the argument is ",(0,i.jsx)(n.code,{children:"null"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"string-checks",children:"String checks"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public void SetCustomerName(string name)\r\n{\r\n Guard.IsNotEmpty(name, nameof(name)); // null, empty, or whitespace\r\n Guard.IsNotOutOfLength(name, 100, nameof(name)); // max length\r\n _name = name;\r\n}\r\n\r\npublic void SetEmail(string email)\r\n{\r\n Guard.IsNotInvalidEmail(email, nameof(email));\r\n _email = email;\r\n}\r\n\r\npublic void SetWebsite(string url)\r\n{\r\n Guard.IsNotInvalidWebUrl(url, nameof(url));\r\n _website = url;\r\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"guid-checks",children:"GUID checks"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public async Task GetOrder(Guid orderId)\r\n{\r\n Guard.IsNotEmpty(orderId, nameof(orderId));\r\n return await _repository.GetByIdAsync(orderId);\r\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"Guard.IsNotEmpty(Guid, string)"})," throws ",(0,i.jsx)(n.code,{children:"ArgumentException"})," when the value is ",(0,i.jsx)(n.code,{children:"Guid.Empty"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["An overload that returns a ",(0,i.jsx)(n.code,{children:"bool"})," instead of throwing is also available when you prefer a conditional check:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"if (!Guard.IsNotEmpty(orderId, nameof(orderId), throwException: false))\r\n{\r\n // handle the case gracefully\r\n return null;\r\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"numeric-checks",children:"Numeric checks"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public void SetPageSize(int pageSize)\r\n{\r\n Guard.IsNotNegative(pageSize, nameof(pageSize)); // < 0 throws\r\n Guard.IsNotNegativeOrZero(pageSize, nameof(pageSize)); // <= 0 throws\r\n}\r\n\r\npublic void SetDiscountRate(decimal rate)\r\n{\r\n Guard.IsNotNegative(rate, nameof(rate));\r\n Guard.IsNotOutOfRange((int)rate, 0, 100, nameof(rate)); // range check (int only)\r\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Numeric overloads exist for ",(0,i.jsx)(n.code,{children:"int"}),", ",(0,i.jsx)(n.code,{children:"long"}),", ",(0,i.jsx)(n.code,{children:"float"}),", ",(0,i.jsx)(n.code,{children:"decimal"}),", and ",(0,i.jsx)(n.code,{children:"TimeSpan"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"date-checks",children:"Date checks"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public void SetDeliveryDate(DateTime deliveryDate)\r\n{\r\n Guard.IsNotInvalidDate(deliveryDate, nameof(deliveryDate));\r\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"collection-checks",children:"Collection checks"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public void ProcessItems(ICollection items)\r\n{\r\n Guard.IsNotEmpty(items, nameof(items)); // null or Count == 0 throws\r\n foreach (var item in items) { /* ... */ }\r\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"arbitrary-condition-checks",children:"Arbitrary condition checks"}),"\n",(0,i.jsxs)(n.p,{children:["Use ",(0,i.jsx)(n.code,{children:"Guard.Against"})," when you need to assert a custom condition and throw a specific exception type:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'Guard.Against(\r\n _isLocked,\r\n "Cannot modify a locked order.");\r\n\r\n// Lambda overload \u2014 evaluated lazily\r\nGuard.Against(\r\n () => order.Status == OrderStatus.Cancelled,\r\n "Cannot add items to a cancelled order.");\n'})}),"\n",(0,i.jsx)(n.h3,{id:"type-and-interface-checks",children:"Type and interface checks"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'Guard.TypeOf(provider, "Provider must implement IPaymentProvider.");\r\nGuard.Implements(providerType, "Type does not implement IPaymentProvider.");\r\nGuard.InheritsFrom(entity, "Entity must inherit from BaseEntity.");\n'})}),"\n",(0,i.jsx)(n.h3,{id:"equality-checks",children:"Equality checks"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'Guard.IsEqual(expectedVersion, actualVersion,\r\n "Version mismatch \u2014 optimistic concurrency violation.");\n'})}),"\n",(0,i.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Method"}),(0,i.jsx)(n.th,{children:"Throws"}),(0,i.jsx)(n.th,{children:"Condition"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotNull(object, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentNullException"})}),(0,i.jsxs)(n.td,{children:["Argument is ",(0,i.jsx)(n.code,{children:"null"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotEmpty(string, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentException"})}),(0,i.jsx)(n.td,{children:"String is null, empty, or whitespace"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotEmpty(string, string, bool)"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"ArgumentException"})," or ",(0,i.jsx)(n.code,{children:"false"})]}),(0,i.jsx)(n.td,{children:"String is null, empty, or whitespace"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotOutOfLength(string, int, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentException"})}),(0,i.jsx)(n.td,{children:"String length exceeds maximum"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotInvalidEmail(string, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentException"})}),(0,i.jsx)(n.td,{children:"String is not a valid email address"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotInvalidWebUrl(string, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentException"})}),(0,i.jsx)(n.td,{children:"String is not a valid web URL"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotEmpty(Guid, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentException"})}),(0,i.jsxs)(n.td,{children:["GUID is ",(0,i.jsx)(n.code,{children:"Guid.Empty"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotEmpty(Guid, string, bool)"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"ArgumentException"})," or ",(0,i.jsx)(n.code,{children:"false"})]}),(0,i.jsxs)(n.td,{children:["GUID is ",(0,i.jsx)(n.code,{children:"Guid.Empty"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotEmpty(ICollection, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentException"})}),(0,i.jsx)(n.td,{children:"Collection is null or empty"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotNegative(int/long/float/decimal/TimeSpan, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentOutOfRangeException"})}),(0,i.jsx)(n.td,{children:"Value is less than zero"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotNegativeOrZero(int/long/float/decimal/TimeSpan, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentOutOfRangeException"})}),(0,i.jsx)(n.td,{children:"Value is zero or less"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotOutOfRange(int, int, int, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentOutOfRangeException"})}),(0,i.jsx)(n.td,{children:"Value is outside [min, max]"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotInvalidDate(DateTime, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentOutOfRangeException"})}),(0,i.jsx)(n.td,{children:"Date is not valid"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Against(bool, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"TException"})}),(0,i.jsxs)(n.td,{children:["Assertion is ",(0,i.jsx)(n.code,{children:"true"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Against(Func, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"TException"})}),(0,i.jsxs)(n.td,{children:["Lambda returns ",(0,i.jsx)(n.code,{children:"true"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"TypeOf(object, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InvalidOperationException"})}),(0,i.jsx)(n.td,{children:"Object is not of expected type"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Implements(object/Type, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InvalidOperationException"})}),(0,i.jsx)(n.td,{children:"Type does not implement interface"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InheritsFrom(object/Type, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InvalidOperationException"})}),(0,i.jsx)(n.td,{children:"Type does not inherit from base"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsEqual(object, object, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"TException"})}),(0,i.jsx)(n.td,{children:"Objects are not equal"})]})]})]})]})}function x(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},75783(e,n,r){r.d(n,{A:()=>a});var i=r(30758);const t="container_xjrG",s="label_Y4p8",c="commandRow_FY5I",d="command_m7Qs",o="copyButton_u1GK";var l=r(86070);function a({packageName:e,version:n}){const[r,a]=(0,i.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,l.jsxs)("div",{className:t,children:[(0,l.jsx)("div",{className:s,children:"NuGet Package"}),(0,l.jsxs)("div",{className:c,children:[(0,l.jsx)("code",{className:d,children:h}),(0,l.jsx)("button",{className:o,onClick:()=>{navigator.clipboard.writeText(h),a(!0),setTimeout(()=>a(!1),2e3)},title:"Copy to clipboard",children:r?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,r){r.d(n,{R:()=>c,x:()=>d});var i=r(30758);const t={},s=i.createContext(t);function c(e){const n=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:c(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/d7ba9c2b.f41cebea.js b/website/build/assets/js/d7ba9c2b.f41cebea.js deleted file mode 100644 index 071e8c7e..00000000 --- a/website/build/assets/js/d7ba9c2b.f41cebea.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[1475],{95550(e,n,r){r.r(n),r.d(n,{assets:()=>l,contentTitle:()=>d,default:()=>x,frontMatter:()=>c,metadata:()=>o,toc:()=>a});var i=r(86070),t=r(81753),s=r(75783);const c={title:"Guards & Validation",sidebar_position:2,description:"Use RCommon Guard clauses for defensive programming \u2014 null checks, string, GUID, numeric, date, collection, type, and custom condition validation in .NET."},d="Guards & Validation",o={id:"core-concepts/guards",title:"Guards & Validation",description:"Use RCommon Guard clauses for defensive programming \u2014 null checks, string, GUID, numeric, date, collection, type, and custom condition validation in .NET.",source:"@site/docs/core-concepts/guards.mdx",sourceDirName:"core-concepts",slug:"/core-concepts/guards",permalink:"/docs/next/core-concepts/guards",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/core-concepts/guards.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Guards & Validation",sidebar_position:2,description:"Use RCommon Guard clauses for defensive programming \u2014 null checks, string, GUID, numeric, date, collection, type, and custom condition validation in .NET."},sidebar:"docsSidebar",previous:{title:"Fluent Configuration",permalink:"/docs/next/core-concepts/fluent-configuration"},next:{title:"GUID Generation",permalink:"/docs/next/core-concepts/guid-generation"}},l={},a=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Usage",id:"usage",level:2},{value:"Null checks",id:"null-checks",level:3},{value:"String checks",id:"string-checks",level:3},{value:"GUID checks",id:"guid-checks",level:3},{value:"Numeric checks",id:"numeric-checks",level:3},{value:"Date checks",id:"date-checks",level:3},{value:"Collection checks",id:"collection-checks",level:3},{value:"Arbitrary condition checks",id:"arbitrary-condition-checks",level:3},{value:"Type and interface checks",id:"type-and-interface-checks",level:3},{value:"Equality checks",id:"equality-checks",level:3},{value:"API Summary",id:"api-summary",level:2}];function h(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"guards--validation",children:"Guards & Validation"}),"\n",(0,i.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsxs)(n.p,{children:["RCommon ships a ",(0,i.jsx)(n.code,{children:"Guard"})," utility class that implements the guard clause pattern. Guard clauses validate inputs at the top of a method and throw a descriptive exception immediately when a precondition is violated, rather than allowing invalid state to propagate further into the call stack."]}),"\n",(0,i.jsx)(n.p,{children:"All methods are static, so no instantiation or dependency injection is required. You call them directly at the start of any method that needs to validate its arguments."}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(s.A,{packageName:"RCommon.Core"}),"\n",(0,i.jsxs)(n.p,{children:["No additional configuration is required. The ",(0,i.jsx)(n.code,{children:"Guard"})," class is part of the ",(0,i.jsx)(n.code,{children:"RCommon"})," namespace and is available as soon as the package is referenced."]}),"\n",(0,i.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(n.h3,{id:"null-checks",children:"Null checks"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\r\n\r\npublic class OrderService\r\n{\r\n private readonly IOrderRepository _repository;\r\n\r\n public OrderService(IOrderRepository repository)\r\n {\r\n Guard.IsNotNull(repository, nameof(repository));\r\n _repository = repository;\r\n }\r\n\r\n public async Task SubmitOrder(Order order)\r\n {\r\n Guard.IsNotNull(order, nameof(order));\r\n await _repository.SaveAsync(order);\r\n }\r\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"Guard.IsNotNull"})," throws ",(0,i.jsx)(n.code,{children:"ArgumentNullException"})," when the argument is ",(0,i.jsx)(n.code,{children:"null"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"string-checks",children:"String checks"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public void SetCustomerName(string name)\r\n{\r\n Guard.IsNotEmpty(name, nameof(name)); // null, empty, or whitespace\r\n Guard.IsNotOutOfLength(name, 100, nameof(name)); // max length\r\n _name = name;\r\n}\r\n\r\npublic void SetEmail(string email)\r\n{\r\n Guard.IsNotInvalidEmail(email, nameof(email));\r\n _email = email;\r\n}\r\n\r\npublic void SetWebsite(string url)\r\n{\r\n Guard.IsNotInvalidWebUrl(url, nameof(url));\r\n _website = url;\r\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"guid-checks",children:"GUID checks"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public async Task GetOrder(Guid orderId)\r\n{\r\n Guard.IsNotEmpty(orderId, nameof(orderId));\r\n return await _repository.GetByIdAsync(orderId);\r\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"Guard.IsNotEmpty(Guid, string)"})," throws ",(0,i.jsx)(n.code,{children:"ArgumentException"})," when the value is ",(0,i.jsx)(n.code,{children:"Guid.Empty"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["An overload that returns a ",(0,i.jsx)(n.code,{children:"bool"})," instead of throwing is also available when you prefer a conditional check:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"if (!Guard.IsNotEmpty(orderId, nameof(orderId), throwException: false))\r\n{\r\n // handle the case gracefully\r\n return null;\r\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"numeric-checks",children:"Numeric checks"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public void SetPageSize(int pageSize)\r\n{\r\n Guard.IsNotNegative(pageSize, nameof(pageSize)); // < 0 throws\r\n Guard.IsNotNegativeOrZero(pageSize, nameof(pageSize)); // <= 0 throws\r\n}\r\n\r\npublic void SetDiscountRate(decimal rate)\r\n{\r\n Guard.IsNotNegative(rate, nameof(rate));\r\n Guard.IsNotOutOfRange((int)rate, 0, 100, nameof(rate)); // range check (int only)\r\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Numeric overloads exist for ",(0,i.jsx)(n.code,{children:"int"}),", ",(0,i.jsx)(n.code,{children:"long"}),", ",(0,i.jsx)(n.code,{children:"float"}),", ",(0,i.jsx)(n.code,{children:"decimal"}),", and ",(0,i.jsx)(n.code,{children:"TimeSpan"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"date-checks",children:"Date checks"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public void SetDeliveryDate(DateTime deliveryDate)\r\n{\r\n Guard.IsNotInvalidDate(deliveryDate, nameof(deliveryDate));\r\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"collection-checks",children:"Collection checks"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public void ProcessItems(ICollection items)\r\n{\r\n Guard.IsNotEmpty(items, nameof(items)); // null or Count == 0 throws\r\n foreach (var item in items) { /* ... */ }\r\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"arbitrary-condition-checks",children:"Arbitrary condition checks"}),"\n",(0,i.jsxs)(n.p,{children:["Use ",(0,i.jsx)(n.code,{children:"Guard.Against"})," when you need to assert a custom condition and throw a specific exception type:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'Guard.Against(\r\n _isLocked,\r\n "Cannot modify a locked order.");\r\n\r\n// Lambda overload \u2014 evaluated lazily\r\nGuard.Against(\r\n () => order.Status == OrderStatus.Cancelled,\r\n "Cannot add items to a cancelled order.");\n'})}),"\n",(0,i.jsx)(n.h3,{id:"type-and-interface-checks",children:"Type and interface checks"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'Guard.TypeOf(provider, "Provider must implement IPaymentProvider.");\r\nGuard.Implements(providerType, "Type does not implement IPaymentProvider.");\r\nGuard.InheritsFrom(entity, "Entity must inherit from BaseEntity.");\n'})}),"\n",(0,i.jsx)(n.h3,{id:"equality-checks",children:"Equality checks"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'Guard.IsEqual(expectedVersion, actualVersion,\r\n "Version mismatch \u2014 optimistic concurrency violation.");\n'})}),"\n",(0,i.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Method"}),(0,i.jsx)(n.th,{children:"Throws"}),(0,i.jsx)(n.th,{children:"Condition"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotNull(object, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentNullException"})}),(0,i.jsxs)(n.td,{children:["Argument is ",(0,i.jsx)(n.code,{children:"null"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotEmpty(string, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentException"})}),(0,i.jsx)(n.td,{children:"String is null, empty, or whitespace"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotEmpty(string, string, bool)"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"ArgumentException"})," or ",(0,i.jsx)(n.code,{children:"false"})]}),(0,i.jsx)(n.td,{children:"String is null, empty, or whitespace"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotOutOfLength(string, int, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentException"})}),(0,i.jsx)(n.td,{children:"String length exceeds maximum"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotInvalidEmail(string, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentException"})}),(0,i.jsx)(n.td,{children:"String is not a valid email address"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotInvalidWebUrl(string, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentException"})}),(0,i.jsx)(n.td,{children:"String is not a valid web URL"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotEmpty(Guid, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentException"})}),(0,i.jsxs)(n.td,{children:["GUID is ",(0,i.jsx)(n.code,{children:"Guid.Empty"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotEmpty(Guid, string, bool)"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"ArgumentException"})," or ",(0,i.jsx)(n.code,{children:"false"})]}),(0,i.jsxs)(n.td,{children:["GUID is ",(0,i.jsx)(n.code,{children:"Guid.Empty"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotEmpty(ICollection, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentException"})}),(0,i.jsx)(n.td,{children:"Collection is null or empty"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotNegative(int/long/float/decimal/TimeSpan, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentOutOfRangeException"})}),(0,i.jsx)(n.td,{children:"Value is less than zero"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotNegativeOrZero(int/long/float/decimal/TimeSpan, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentOutOfRangeException"})}),(0,i.jsx)(n.td,{children:"Value is zero or less"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotOutOfRange(int, int, int, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentOutOfRangeException"})}),(0,i.jsx)(n.td,{children:"Value is outside [min, max]"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsNotInvalidDate(DateTime, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ArgumentOutOfRangeException"})}),(0,i.jsx)(n.td,{children:"Date is not valid"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Against(bool, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"TException"})}),(0,i.jsxs)(n.td,{children:["Assertion is ",(0,i.jsx)(n.code,{children:"true"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Against(Func, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"TException"})}),(0,i.jsxs)(n.td,{children:["Lambda returns ",(0,i.jsx)(n.code,{children:"true"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"TypeOf(object, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InvalidOperationException"})}),(0,i.jsx)(n.td,{children:"Object is not of expected type"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Implements(object/Type, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InvalidOperationException"})}),(0,i.jsx)(n.td,{children:"Type does not implement interface"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InheritsFrom(object/Type, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"InvalidOperationException"})}),(0,i.jsx)(n.td,{children:"Type does not inherit from base"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IsEqual(object, object, string)"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"TException"})}),(0,i.jsx)(n.td,{children:"Objects are not equal"})]})]})]})]})}function x(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},75783(e,n,r){r.d(n,{A:()=>a});var i=r(30758);const t="container_xjrG",s="label_Y4p8",c="commandRow_FY5I",d="command_m7Qs",o="copyButton_u1GK";var l=r(86070);function a({packageName:e,version:n}){const[r,a]=(0,i.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,l.jsxs)("div",{className:t,children:[(0,l.jsx)("div",{className:s,children:"NuGet Package"}),(0,l.jsxs)("div",{className:c,children:[(0,l.jsx)("code",{className:d,children:h}),(0,l.jsx)("button",{className:o,onClick:()=>{navigator.clipboard.writeText(h),a(!0),setTimeout(()=>a(!1),2e3)},title:"Copy to clipboard",children:r?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,r){r.d(n,{R:()=>c,x:()=>d});var i=r(30758);const t={},s=i.createContext(t);function c(e){const n=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:c(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/da584be8.4ce44815.js b/website/build/assets/js/da584be8.793fc1b9.js similarity index 56% rename from website/build/assets/js/da584be8.4ce44815.js rename to website/build/assets/js/da584be8.793fc1b9.js index 2d79a2db..bddcf7d1 100644 --- a/website/build/assets/js/da584be8.4ce44815.js +++ b/website/build/assets/js/da584be8.793fc1b9.js @@ -1 +1 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[584],{68218(e,r,n){n.r(r),n.d(r,{assets:()=>a,contentTitle:()=>o,default:()=>u,frontMatter:()=>c,metadata:()=>d,toc:()=>l});var i=n(86070),t=n(81753),s=n(75783);const c={title:"Web Utilities",sidebar_position:2},o="Web Utilities",d={id:"security-web/web-utilities",title:"Web Utilities",description:"Overview",source:"@site/versioned_docs/version-2.4.1/security-web/web-utilities.mdx",sourceDirName:"security-web",slug:"/security-web/web-utilities",permalink:"/docs/security-web/web-utilities",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/versioned_docs/version-2.4.1/security-web/web-utilities.mdx",tags:[],version:"2.4.1",sidebarPosition:2,frontMatter:{title:"Web Utilities",sidebar_position:2},sidebar:"docsSidebar",previous:{title:"Authorization",permalink:"/docs/security-web/authorization"},next:{title:"Architecture Guides",permalink:"/docs/category/architecture-guides"}},a={},l=[{value:"Overview",id:"overview",level:2},{value:"Why a separate web package?",id:"why-a-separate-web-package",level:3},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Combined with other RCommon features",id:"combined-with-other-rcommon-features",level:3},{value:"Usage",id:"usage",level:2},{value:"Injecting ICurrentUser in a controller or service",id:"injecting-icurrentuser-in-a-controller-or-service",level:3},{value:"Using ICurrentUser in middleware",id:"using-icurrentuser-in-middleware",level:3},{value:"Temporarily overriding the principal",id:"temporarily-overriding-the-principal",level:3},{value:"Switching from ThreadCurrentPrincipalAccessor",id:"switching-from-threadcurrentprincipalaccessor",level:3},{value:"API Summary",id:"api-summary",level:2}];function h(e){const r={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(r.h1,{id:"web-utilities",children:"Web Utilities"}),"\n",(0,i.jsx)(r.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"RCommon.Web"})," provides ASP.NET Core-specific integrations that bridge RCommon's security abstractions to the HTTP request pipeline. Its primary purpose is to replace the thread-based principal accessor with one that reads the authenticated user from the current ",(0,i.jsx)(r.code,{children:"HttpContext"}),", which is the correct source of identity in ASP.NET Core web applications."]}),"\n",(0,i.jsx)(r.p,{children:"The package contains:"}),"\n",(0,i.jsxs)(r.ul,{children:["\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"})," \u2014 an ",(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})," implementation that reads ",(0,i.jsx)(r.code,{children:"HttpContext.User"})," via ",(0,i.jsx)(r.code,{children:"IHttpContextAccessor"}),"."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"WebConfigurationExtensions"})," \u2014 a startup extension method that registers all security services using the HTTP-context-based accessor."]}),"\n"]}),"\n",(0,i.jsx)(r.h3,{id:"why-a-separate-web-package",children:"Why a separate web package?"}),"\n",(0,i.jsxs)(r.p,{children:["RCommon's core security library (",(0,i.jsx)(r.code,{children:"RCommon.Security"}),") has no ASP.NET Core dependency. Its default principal accessor reads from ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"}),", which works in console apps, background services, and test harnesses. In an ASP.NET Core web app the authenticated user is set on ",(0,i.jsx)(r.code,{children:"HttpContext.User"})," by the authentication middleware, not on ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"}),". ",(0,i.jsx)(r.code,{children:"RCommon.Web"})," provides the adapter that makes the correct source available to all RCommon services."]}),"\n",(0,i.jsx)(r.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(s.A,{packageName:"RCommon.Web"}),"\n",(0,i.jsxs)(r.p,{children:["This package depends on ",(0,i.jsx)(r.code,{children:"RCommon.Security"}),", which is pulled in automatically."]}),"\n",(0,i.jsx)(r.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(r.p,{children:["Call ",(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," instead of ",(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessor()"})," in your ASP.NET Core startup:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"using RCommon;\r\n\r\nvar builder = WebApplication.CreateBuilder(args);\r\n\r\nbuilder.Services.AddRCommon(config =>\r\n{\r\n config.WithClaimsAndPrincipalAccessorForWeb();\r\n});\r\n\r\nvar app = builder.Build();\r\n\r\n// Ensure authentication and authorization middleware run before your endpoints.\r\napp.UseAuthentication();\r\napp.UseAuthorization();\r\n\r\napp.MapControllers();\r\napp.Run();\n"})}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," registers the following services:"]}),"\n",(0,i.jsxs)(r.table,{children:[(0,i.jsx)(r.thead,{children:(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.th,{children:"Interface"}),(0,i.jsx)(r.th,{children:"Implementation"})]})}),(0,i.jsxs)(r.tbody,{children:[(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"})})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentUser"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentUser"})})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentClient"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentClient"})})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ITenantIdAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ClaimsTenantIdAccessor"})})]})]})]}),"\n",(0,i.jsxs)(r.p,{children:["It also calls ",(0,i.jsx)(r.code,{children:"services.AddHttpContextAccessor()"})," so that ",(0,i.jsx)(r.code,{children:"IHttpContextAccessor"})," is available for injection."]}),"\n",(0,i.jsx)(r.h3,{id:"combined-with-other-rcommon-features",children:"Combined with other RCommon features"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," is fluent and chains naturally with persistence, multi-tenancy, and other configuration:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'builder.Services.AddRCommon(config =>\r\n{\r\n config\r\n .WithClaimsAndPrincipalAccessorForWeb()\r\n .WithPersistence(ef =>\r\n {\r\n ef.AddDbContext(\r\n "App",\r\n options => options.UseSqlServer(\r\n builder.Configuration.GetConnectionString("Default")));\r\n })\r\n .WithMultiTenancy>(mt => { });\r\n});\n'})}),"\n",(0,i.jsx)(r.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(r.h3,{id:"injecting-icurrentuser-in-a-controller-or-service",children:"Injecting ICurrentUser in a controller or service"}),"\n",(0,i.jsxs)(r.p,{children:["Once ",(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," is registered, inject ",(0,i.jsx)(r.code,{children:"ICurrentUser"})," anywhere in your application. The resolved identity comes from the authenticated ",(0,i.jsx)(r.code,{children:"HttpContext.User"})," for the current request:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Users;\r\nusing Microsoft.AspNetCore.Authorization;\r\nusing Microsoft.AspNetCore.Mvc;\r\n\r\n[Authorize]\r\n[ApiController]\r\n[Route("api/[controller]")]\r\npublic class ProfileController : ControllerBase\r\n{\r\n private readonly ICurrentUser _currentUser;\r\n\r\n public ProfileController(ICurrentUser currentUser)\r\n {\r\n _currentUser = currentUser;\r\n }\r\n\r\n [HttpGet]\r\n public IActionResult GetProfile()\r\n {\r\n return Ok(new\r\n {\r\n UserId = _currentUser.Id,\r\n Roles = _currentUser.Roles,\r\n TenantId = _currentUser.TenantId\r\n });\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"using-icurrentuser-in-middleware",children:"Using ICurrentUser in middleware"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Users;\r\n\r\npublic class RequestAuditMiddleware\r\n{\r\n private readonly RequestDelegate _next;\r\n\r\n public RequestAuditMiddleware(RequestDelegate next)\r\n {\r\n _next = next;\r\n }\r\n\r\n public async Task InvokeAsync(HttpContext context, ICurrentUser currentUser)\r\n {\r\n if (currentUser.IsAuthenticated)\r\n {\r\n var userId = currentUser.Id?.ToString() ?? "anonymous";\r\n context.Items["AuditUserId"] = userId;\r\n }\r\n\r\n await _next(context);\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"temporarily-overriding-the-principal",children:"Temporarily overriding the principal"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"})," inherits the ",(0,i.jsx)(r.code,{children:"Change()"})," method from ",(0,i.jsx)(r.code,{children:"CurrentPrincipalAccessorBase"}),". The override is stored in ",(0,i.jsx)(r.code,{children:"AsyncLocal"})," and does not mutate ",(0,i.jsx)(r.code,{children:"HttpContext.User"}),", making it safe to use in middleware or background tasks that need to act as a different identity:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Claims;\r\nusing System.Security.Claims;\r\n\r\npublic class IntegrationEventHandler\r\n{\r\n private readonly ICurrentPrincipalAccessor _principalAccessor;\r\n private readonly ICurrentUser _currentUser;\r\n\r\n public IntegrationEventHandler(\r\n ICurrentPrincipalAccessor principalAccessor,\r\n ICurrentUser currentUser)\r\n {\r\n _principalAccessor = principalAccessor;\r\n _currentUser = currentUser;\r\n }\r\n\r\n public async Task HandleAsync(IntegrationEvent @event)\r\n {\r\n // Run as a system identity when processing out-of-band events.\r\n var systemIdentity = new ClaimsIdentity(new[]\r\n {\r\n new Claim(ClaimTypesConst.UserId, @event.InitiatingUserId.ToString()),\r\n new Claim(ClaimTypesConst.TenantId, @event.TenantId)\r\n }, "System");\r\n\r\n using (_principalAccessor.Change(new ClaimsPrincipal(systemIdentity)))\r\n {\r\n // ICurrentUser now resolves against systemIdentity.\r\n Console.WriteLine($"Processing as user {_currentUser.Id} for tenant {_currentUser.TenantId}");\r\n await ProcessEventAsync(@event);\r\n }\r\n // Original principal (or null outside a request) is restored here.\r\n }\r\n\r\n private Task ProcessEventAsync(IntegrationEvent @event) => Task.CompletedTask;\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"switching-from-threadcurrentprincipalaccessor",children:"Switching from ThreadCurrentPrincipalAccessor"}),"\n",(0,i.jsxs)(r.p,{children:["If you started with ",(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessor()"})," and are migrating to an ASP.NET Core host, replace it with ",(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"}),". The registered interfaces are identical; only the ",(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})," implementation changes:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"// Before (non-web or background service):\r\nconfig.WithClaimsAndPrincipalAccessor();\r\n\r\n// After (ASP.NET Core web application):\r\nconfig.WithClaimsAndPrincipalAccessorForWeb();\n"})}),"\n",(0,i.jsxs)(r.p,{children:["No other code changes are needed. All services that depend on ",(0,i.jsx)(r.code,{children:"ICurrentUser"}),", ",(0,i.jsx)(r.code,{children:"ICurrentClient"}),", or ",(0,i.jsx)(r.code,{children:"ITenantIdAccessor"})," continue to work without modification."]}),"\n",(0,i.jsx)(r.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(r.table,{children:[(0,i.jsx)(r.thead,{children:(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.th,{children:"Type"}),(0,i.jsx)(r.th,{children:"Package"}),(0,i.jsx)(r.th,{children:"Description"})]})}),(0,i.jsxs)(r.tbody,{children:[(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Web"})}),(0,i.jsxs)(r.td,{children:["Reads the current ",(0,i.jsx)(r.code,{children:"ClaimsPrincipal"})," from ",(0,i.jsx)(r.code,{children:"IHttpContextAccessor.HttpContext.User"}),"; falls back to ",(0,i.jsx)(r.code,{children:"null"})," outside of a request"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"WebConfigurationExtensions.WithClaimsAndPrincipalAccessorForWeb"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Web"})}),(0,i.jsxs)(r.td,{children:["Registers ",(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"}),", ",(0,i.jsx)(r.code,{children:"ICurrentUser"}),", ",(0,i.jsx)(r.code,{children:"ICurrentClient"}),", ",(0,i.jsx)(r.code,{children:"ITenantIdAccessor"}),", and ",(0,i.jsx)(r.code,{children:"IHttpContextAccessor"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Core abstraction; exposes ",(0,i.jsx)(r.code,{children:"Principal"})," and ",(0,i.jsx)(r.code,{children:"Change(ClaimsPrincipal)"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentPrincipalAccessorBase"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Abstract base; ",(0,i.jsx)(r.code,{children:"Change()"})," stores the override in ",(0,i.jsx)(r.code,{children:"AsyncLocal"})," so it flows across async continuations"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ThreadCurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Non-web default; reads ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"})]})]})]})]})]})}function u(e={}){const{wrapper:r}={...(0,t.R)(),...e.components};return r?(0,i.jsx)(r,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},75783(e,r,n){n.d(r,{A:()=>l});var i=n(30758);const t="container_xjrG",s="label_Y4p8",c="commandRow_FY5I",o="command_m7Qs",d="copyButton_u1GK";var a=n(86070);function l({packageName:e,version:r}){const[n,l]=(0,i.useState)(!1),h=r?`dotnet add package ${e} --version ${r}`:`dotnet add package ${e}`;return(0,a.jsxs)("div",{className:t,children:[(0,a.jsx)("div",{className:s,children:"NuGet Package"}),(0,a.jsxs)("div",{className:c,children:[(0,a.jsx)("code",{className:o,children:h}),(0,a.jsx)("button",{className:d,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:n?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,r,n){n.d(r,{R:()=>c,x:()=>o});var i=n(30758);const t={},s=i.createContext(t);function c(e){const r=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(r):{...r,...e}},[r,e])}function o(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:c(e.components),i.createElement(s.Provider,{value:r},e.children)}}}]); \ No newline at end of file +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[584],{68218(e,r,n){n.r(r),n.d(r,{assets:()=>a,contentTitle:()=>o,default:()=>u,frontMatter:()=>c,metadata:()=>d,toc:()=>l});var i=n(86070),t=n(81753),s=n(75783);const c={title:"Web Utilities",sidebar_position:2},o="Web Utilities",d={id:"security-web/web-utilities",title:"Web Utilities",description:"Overview",source:"@site/versioned_docs/version-2.4.1/security-web/web-utilities.mdx",sourceDirName:"security-web",slug:"/security-web/web-utilities",permalink:"/docs/security-web/web-utilities",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/versioned_docs/version-2.4.1/security-web/web-utilities.mdx",tags:[],version:"2.4.1",sidebarPosition:2,frontMatter:{title:"Web Utilities",sidebar_position:2},sidebar:"docsSidebar",previous:{title:"Authorization",permalink:"/docs/security-web/authorization"},next:{title:"Architecture Guides",permalink:"/docs/category/architecture-guides"}},a={},l=[{value:"Overview",id:"overview",level:2},{value:"Why a separate web package?",id:"why-a-separate-web-package",level:3},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Combined with other RCommon features",id:"combined-with-other-rcommon-features",level:3},{value:"Usage",id:"usage",level:2},{value:"Injecting ICurrentUser in a controller or service",id:"injecting-icurrentuser-in-a-controller-or-service",level:3},{value:"Using ICurrentUser in middleware",id:"using-icurrentuser-in-middleware",level:3},{value:"Temporarily overriding the principal",id:"temporarily-overriding-the-principal",level:3},{value:"Switching from ThreadCurrentPrincipalAccessor",id:"switching-from-threadcurrentprincipalaccessor",level:3},{value:"API Summary",id:"api-summary",level:2}];function h(e){const r={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(r.h1,{id:"web-utilities",children:"Web Utilities"}),"\n",(0,i.jsx)(r.h2,{id:"overview",children:"Overview"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"RCommon.Web"})," provides ASP.NET Core-specific integrations that bridge RCommon's security abstractions to the HTTP request pipeline. Its primary purpose is to replace the thread-based principal accessor with one that reads the authenticated user from the current ",(0,i.jsx)(r.code,{children:"HttpContext"}),", which is the correct source of identity in ASP.NET Core web applications."]}),"\n",(0,i.jsx)(r.p,{children:"The package contains:"}),"\n",(0,i.jsxs)(r.ul,{children:["\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"})," \u2014 an ",(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})," implementation that reads ",(0,i.jsx)(r.code,{children:"HttpContext.User"})," via ",(0,i.jsx)(r.code,{children:"IHttpContextAccessor"}),"."]}),"\n",(0,i.jsxs)(r.li,{children:[(0,i.jsx)(r.code,{children:"WebConfigurationExtensions"})," \u2014 a startup extension method that registers all security services using the HTTP-context-based accessor."]}),"\n"]}),"\n",(0,i.jsx)(r.h3,{id:"why-a-separate-web-package",children:"Why a separate web package?"}),"\n",(0,i.jsxs)(r.p,{children:["RCommon's core security library (",(0,i.jsx)(r.code,{children:"RCommon.Security"}),") has no ASP.NET Core dependency. Its default principal accessor reads from ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"}),", which works in console apps, background services, and test harnesses. In an ASP.NET Core web app the authenticated user is set on ",(0,i.jsx)(r.code,{children:"HttpContext.User"})," by the authentication middleware, not on ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"}),". ",(0,i.jsx)(r.code,{children:"RCommon.Web"})," provides the adapter that makes the correct source available to all RCommon services."]}),"\n",(0,i.jsx)(r.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(s.A,{packageName:"RCommon.Web"}),"\n",(0,i.jsxs)(r.p,{children:["This package depends on ",(0,i.jsx)(r.code,{children:"RCommon.Security"}),", which is pulled in automatically."]}),"\n",(0,i.jsx)(r.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(r.p,{children:["Call ",(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," instead of ",(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessor()"})," in your ASP.NET Core startup:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"using RCommon;\r\n\r\nvar builder = WebApplication.CreateBuilder(args);\r\n\r\nbuilder.Services.AddRCommon(config =>\r\n{\r\n config.WithClaimsAndPrincipalAccessorForWeb();\r\n});\r\n\r\nvar app = builder.Build();\r\n\r\n// Ensure authentication and authorization middleware run before your endpoints.\r\napp.UseAuthentication();\r\napp.UseAuthorization();\r\n\r\napp.MapControllers();\r\napp.Run();\n"})}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," registers the following services:"]}),"\n",(0,i.jsxs)(r.table,{children:[(0,i.jsx)(r.thead,{children:(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.th,{children:"Interface"}),(0,i.jsx)(r.th,{children:"Implementation"})]})}),(0,i.jsxs)(r.tbody,{children:[(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"})})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentUser"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentUser"})})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentClient"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentClient"})})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ITenantIdAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ClaimsTenantIdAccessor"})})]})]})]}),"\n",(0,i.jsxs)(r.p,{children:["It also calls ",(0,i.jsx)(r.code,{children:"services.AddHttpContextAccessor()"})," so that ",(0,i.jsx)(r.code,{children:"IHttpContextAccessor"})," is available for injection."]}),"\n",(0,i.jsx)(r.h3,{id:"combined-with-other-rcommon-features",children:"Combined with other RCommon features"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," is fluent and chains naturally with persistence, multi-tenancy, and other configuration:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'builder.Services.AddRCommon(config =>\r\n{\r\n config\r\n .WithClaimsAndPrincipalAccessorForWeb()\r\n .WithPersistence(ef =>\r\n {\r\n ef.AddDbContext(\r\n "App",\r\n options => options.UseSqlServer(\r\n builder.Configuration.GetConnectionString("Default")));\r\n })\r\n .WithMultiTenancy>(mt => { });\r\n});\n'})}),"\n",(0,i.jsx)(r.h2,{id:"usage",children:"Usage"}),"\n",(0,i.jsx)(r.h3,{id:"injecting-icurrentuser-in-a-controller-or-service",children:"Injecting ICurrentUser in a controller or service"}),"\n",(0,i.jsxs)(r.p,{children:["Once ",(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"})," is registered, inject ",(0,i.jsx)(r.code,{children:"ICurrentUser"})," anywhere in your application. The resolved identity comes from the authenticated ",(0,i.jsx)(r.code,{children:"HttpContext.User"})," for the current request:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Users;\r\nusing Microsoft.AspNetCore.Authorization;\r\nusing Microsoft.AspNetCore.Mvc;\r\n\r\n[Authorize]\r\n[ApiController]\r\n[Route("api/[controller]")]\r\npublic class ProfileController : ControllerBase\r\n{\r\n private readonly ICurrentUser _currentUser;\r\n\r\n public ProfileController(ICurrentUser currentUser)\r\n {\r\n _currentUser = currentUser;\r\n }\r\n\r\n [HttpGet]\r\n public IActionResult GetProfile()\r\n {\r\n return Ok(new\r\n {\r\n UserId = _currentUser.Id,\r\n Roles = _currentUser.Roles,\r\n TenantId = _currentUser.TenantId\r\n });\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"using-icurrentuser-in-middleware",children:"Using ICurrentUser in middleware"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Users;\r\n\r\npublic class RequestAuditMiddleware\r\n{\r\n private readonly RequestDelegate _next;\r\n\r\n public RequestAuditMiddleware(RequestDelegate next)\r\n {\r\n _next = next;\r\n }\r\n\r\n public async Task InvokeAsync(HttpContext context, ICurrentUser currentUser)\r\n {\r\n if (currentUser.IsAuthenticated)\r\n {\r\n var userId = currentUser.Id ?? "anonymous";\r\n context.Items["AuditUserId"] = userId;\r\n }\r\n\r\n await _next(context);\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"temporarily-overriding-the-principal",children:"Temporarily overriding the principal"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"})," inherits the ",(0,i.jsx)(r.code,{children:"Change()"})," method from ",(0,i.jsx)(r.code,{children:"CurrentPrincipalAccessorBase"}),". The override is stored in ",(0,i.jsx)(r.code,{children:"AsyncLocal"})," and does not mutate ",(0,i.jsx)(r.code,{children:"HttpContext.User"}),", making it safe to use in middleware or background tasks that need to act as a different identity:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'using RCommon.Security.Claims;\r\nusing System.Security.Claims;\r\n\r\npublic class IntegrationEventHandler\r\n{\r\n private readonly ICurrentPrincipalAccessor _principalAccessor;\r\n private readonly ICurrentUser _currentUser;\r\n\r\n public IntegrationEventHandler(\r\n ICurrentPrincipalAccessor principalAccessor,\r\n ICurrentUser currentUser)\r\n {\r\n _principalAccessor = principalAccessor;\r\n _currentUser = currentUser;\r\n }\r\n\r\n public async Task HandleAsync(IntegrationEvent @event)\r\n {\r\n // Run as a system identity when processing out-of-band events.\r\n var systemIdentity = new ClaimsIdentity(new[]\r\n {\r\n new Claim(ClaimTypesConst.UserId, @event.InitiatingUserId.ToString()),\r\n new Claim(ClaimTypesConst.TenantId, @event.TenantId)\r\n }, "System");\r\n\r\n using (_principalAccessor.Change(new ClaimsPrincipal(systemIdentity)))\r\n {\r\n // ICurrentUser now resolves against systemIdentity.\r\n Console.WriteLine($"Processing as user {_currentUser.Id} for tenant {_currentUser.TenantId}");\r\n await ProcessEventAsync(@event);\r\n }\r\n // Original principal (or null outside a request) is restored here.\r\n }\r\n\r\n private Task ProcessEventAsync(IntegrationEvent @event) => Task.CompletedTask;\r\n}\n'})}),"\n",(0,i.jsx)(r.h3,{id:"switching-from-threadcurrentprincipalaccessor",children:"Switching from ThreadCurrentPrincipalAccessor"}),"\n",(0,i.jsxs)(r.p,{children:["If you started with ",(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessor()"})," and are migrating to an ASP.NET Core host, replace it with ",(0,i.jsx)(r.code,{children:"WithClaimsAndPrincipalAccessorForWeb()"}),". The registered interfaces are identical; only the ",(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})," implementation changes:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"// Before (non-web or background service):\r\nconfig.WithClaimsAndPrincipalAccessor();\r\n\r\n// After (ASP.NET Core web application):\r\nconfig.WithClaimsAndPrincipalAccessorForWeb();\n"})}),"\n",(0,i.jsxs)(r.p,{children:["No other code changes are needed. All services that depend on ",(0,i.jsx)(r.code,{children:"ICurrentUser"}),", ",(0,i.jsx)(r.code,{children:"ICurrentClient"}),", or ",(0,i.jsx)(r.code,{children:"ITenantIdAccessor"})," continue to work without modification."]}),"\n",(0,i.jsx)(r.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,i.jsxs)(r.table,{children:[(0,i.jsx)(r.thead,{children:(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.th,{children:"Type"}),(0,i.jsx)(r.th,{children:"Package"}),(0,i.jsx)(r.th,{children:"Description"})]})}),(0,i.jsxs)(r.tbody,{children:[(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Web"})}),(0,i.jsxs)(r.td,{children:["Reads the current ",(0,i.jsx)(r.code,{children:"ClaimsPrincipal"})," from ",(0,i.jsx)(r.code,{children:"IHttpContextAccessor.HttpContext.User"}),"; falls back to ",(0,i.jsx)(r.code,{children:"null"})," outside of a request"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"WebConfigurationExtensions.WithClaimsAndPrincipalAccessorForWeb"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Web"})}),(0,i.jsxs)(r.td,{children:["Registers ",(0,i.jsx)(r.code,{children:"HttpContextCurrentPrincipalAccessor"}),", ",(0,i.jsx)(r.code,{children:"ICurrentUser"}),", ",(0,i.jsx)(r.code,{children:"ICurrentClient"}),", ",(0,i.jsx)(r.code,{children:"ITenantIdAccessor"}),", and ",(0,i.jsx)(r.code,{children:"IHttpContextAccessor"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Core abstraction; exposes ",(0,i.jsx)(r.code,{children:"Principal"})," and ",(0,i.jsx)(r.code,{children:"Change(ClaimsPrincipal)"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"CurrentPrincipalAccessorBase"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Abstract base; ",(0,i.jsx)(r.code,{children:"Change()"})," stores the override in ",(0,i.jsx)(r.code,{children:"AsyncLocal"})," so it flows across async continuations"]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ThreadCurrentPrincipalAccessor"})}),(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"RCommon.Security"})}),(0,i.jsxs)(r.td,{children:["Non-web default; reads ",(0,i.jsx)(r.code,{children:"Thread.CurrentPrincipal"})]})]})]})]})]})}function u(e={}){const{wrapper:r}={...(0,t.R)(),...e.components};return r?(0,i.jsx)(r,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},75783(e,r,n){n.d(r,{A:()=>l});var i=n(30758);const t="container_xjrG",s="label_Y4p8",c="commandRow_FY5I",o="command_m7Qs",d="copyButton_u1GK";var a=n(86070);function l({packageName:e,version:r}){const[n,l]=(0,i.useState)(!1),h=r?`dotnet add package ${e} --version ${r}`:`dotnet add package ${e}`;return(0,a.jsxs)("div",{className:t,children:[(0,a.jsx)("div",{className:s,children:"NuGet Package"}),(0,a.jsxs)("div",{className:c,children:[(0,a.jsx)("code",{className:o,children:h}),(0,a.jsx)("button",{className:d,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:n?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,r,n){n.d(r,{R:()=>c,x:()=>o});var i=n(30758);const t={},s=i.createContext(t);function c(e){const r=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(r):{...r,...e}},[r,e])}function o(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:c(e.components),i.createElement(s.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/e0146154.2a6d494f.js b/website/build/assets/js/e0146154.2a6d494f.js new file mode 100644 index 00000000..c656bbb5 --- /dev/null +++ b/website/build/assets/js/e0146154.2a6d494f.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[139],{19306(e,n,r){r.r(n),r.d(n,{assets:()=>c,contentTitle:()=>d,default:()=>u,frontMatter:()=>o,metadata:()=>l,toc:()=>a});var i=r(86070),t=r(81753),s=r(75783);const o={title:"Wolverine",sidebar_position:7,description:"Use Wolverine as an event transport in RCommon's event handling pipeline, registering ISubscriber handlers and choosing between fan-out publish or point-to-point send."},d="Wolverine",l={id:"event-handling/wolverine",title:"Wolverine",description:"Use Wolverine as an event transport in RCommon's event handling pipeline, registering ISubscriber handlers and choosing between fan-out publish or point-to-point send.",source:"@site/docs/event-handling/wolverine.mdx",sourceDirName:"event-handling",slug:"/event-handling/wolverine",permalink:"/docs/next/event-handling/wolverine",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/event-handling/wolverine.mdx",tags:[],version:"current",sidebarPosition:7,frontMatter:{title:"Wolverine",sidebar_position:7,description:"Use Wolverine as an event transport in RCommon's event handling pipeline, registering ISubscriber handlers and choosing between fan-out publish or point-to-point send."},sidebar:"docsSidebar",previous:{title:"MassTransit",permalink:"/docs/next/event-handling/masstransit"},next:{title:"Messaging",permalink:"/docs/next/category/messaging"}},c={},a=[{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Defining an event",id:"defining-an-event",level:2},{value:"Implementing a subscriber",id:"implementing-a-subscriber",level:2},{value:"Publishing events",id:"publishing-events",level:2},{value:"Publish vs Send",id:"publish-vs-send",level:2},{value:"Transactional outbox",id:"transactional-outbox",level:2},{value:"How the handler bridge works",id:"how-the-handler-bridge-works",level:2},{value:"API summary",id:"api-summary",level:2}];function h(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"wolverine",children:"Wolverine"}),"\n",(0,i.jsxs)(n.p,{children:["RCommon integrates with Wolverine to deliver events both in-process and across service boundaries. The integration registers ",(0,i.jsx)(n.code,{children:"WolverineEventHandler"})," as a Wolverine ",(0,i.jsx)(n.code,{children:"IWolverineHandler"})," that delegates to the application's ",(0,i.jsx)(n.code,{children:"ISubscriber"}),". Application handler code has no dependency on Wolverine types."]}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(s.A,{packageName:"RCommon.Wolverine"}),"\n",(0,i.jsxs)(n.p,{children:["Wolverine requires host-level registration via ",(0,i.jsx)(n.code,{children:"UseWolverine()"})," in addition to the RCommon service configuration:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"builder.Host.UseWolverine(opts =>\n{\n // Wolverine transport and endpoint options\n});\n"})}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["Use ",(0,i.jsx)(n.code,{children:"WithEventHandling"})," on the RCommon builder:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\nusing RCommon.Wolverine;\nusing RCommon.Wolverine.Producers;\n\nbuilder.Services.AddRCommon()\n .WithEventHandling(wlv =>\n {\n // Register the producer that publishes events via Wolverine\n wlv.AddProducer();\n\n // Register subscribers and record routing information\n wlv.AddSubscriber();\n wlv.AddSubscriber();\n });\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"AddSubscriber"})," registers ",(0,i.jsx)(n.code,{children:"ISubscriber"})," as scoped in the DI container and records the event-to-producer association in the ",(0,i.jsx)(n.code,{children:"EventSubscriptionManager"})," so the router routes this event only to Wolverine producers."]}),"\n",(0,i.jsx)(n.admonition,{title:"Modular composition",type:"tip",children:(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"WithEventHandling"})," is a cache-aware sub-builder verb. When multiple modules call it, the cached ",(0,i.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})," is reused and each module's ",(0,i.jsx)(n.code,{children:"AddSubscriber"})," and ",(0,i.jsx)(n.code,{children:"AddProducer"})," calls accumulate on the same instance. ",(0,i.jsx)(n.code,{children:"AddProducer"})," deduplicates by concrete producer type \u2014 registering ",(0,i.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})," from two modules yields one descriptor. The host-level ",(0,i.jsx)(n.code,{children:"builder.Host.UseWolverine(...)"})," registration must remain in the application host's composition root; only the RCommon sub-builder participates in module-level composition. See ",(0,i.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,i.jsx)(n.h2,{id:"defining-an-event",children:"Defining an event"}),"\n",(0,i.jsxs)(n.p,{children:["Events must implement ",(0,i.jsx)(n.code,{children:"ISerializableEvent"}),". Include a parameterless constructor for Wolverine deserialization:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Models.Events;\n\npublic class OrderShipped : ISyncEvent\n{\n public OrderShipped(Guid orderId, string trackingNumber)\n {\n OrderId = orderId;\n TrackingNumber = trackingNumber;\n }\n\n public OrderShipped() { }\n\n public Guid OrderId { get; }\n public string TrackingNumber { get; } = string.Empty;\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"implementing-a-subscriber",children:"Implementing a subscriber"}),"\n",(0,i.jsxs)(n.p,{children:["Implement ",(0,i.jsx)(n.code,{children:"ISubscriber"}),". No Wolverine types appear in handler code:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'using RCommon.EventHandling.Subscribers;\n\npublic class OrderShippedHandler : ISubscriber\n{\n private readonly ILogger _logger;\n\n public OrderShippedHandler(ILogger logger)\n {\n _logger = logger;\n }\n\n public async Task HandleAsync(OrderShipped @event, CancellationToken cancellationToken = default)\n {\n _logger.LogInformation(\n "Order {OrderId} shipped with tracking {TrackingNumber}",\n @event.OrderId, @event.TrackingNumber);\n await Task.CompletedTask;\n }\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"publishing-events",children:"Publishing events"}),"\n",(0,i.jsxs)(n.p,{children:["Call ",(0,i.jsx)(n.code,{children:"IEventRouter.RouteEventsAsync"})," after queuing events. The router forwards each event to ",(0,i.jsx)(n.code,{children:"PublishWithWolverineEventProducer"}),", which calls ",(0,i.jsx)(n.code,{children:"IMessageBus.PublishAsync"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class ShippingService\n{\n private readonly IEventRouter _eventRouter;\n\n public ShippingService(IEventRouter eventRouter)\n {\n _eventRouter = eventRouter;\n }\n\n public async Task ShipOrderAsync(Guid orderId, string trackingNumber, CancellationToken cancellationToken)\n {\n // ... update order state ...\n\n _eventRouter.AddTransactionalEvent(new OrderShipped(orderId, trackingNumber));\n await _eventRouter.RouteEventsAsync(cancellationToken);\n }\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"publish-vs-send",children:"Publish vs Send"}),"\n",(0,i.jsxs)(n.p,{children:["Register ",(0,i.jsx)(n.code,{children:"SendWithWolverineEventProducer"})," instead of (or in addition to) ",(0,i.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})," when you want point-to-point delivery to a single handler endpoint:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"wlv.AddProducer();\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"SendWithWolverineEventProducer"})," calls ",(0,i.jsx)(n.code,{children:"IMessageBus.SendAsync"})," internally."]}),"\n",(0,i.jsx)(n.h2,{id:"transactional-outbox",children:"Transactional outbox"}),"\n",(0,i.jsxs)(n.p,{children:["Pair Wolverine with the outbox pattern to guarantee at-least-once delivery. See ",(0,i.jsx)(n.a,{href:"/docs/next/event-handling/transactional-outbox#wolverine-outbox",children:"Transactional Outbox"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"how-the-handler-bridge-works",children:"How the handler bridge works"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"WolverineEventHandler"})," implements both ",(0,i.jsx)(n.code,{children:"IWolverineEventHandler"})," and Wolverine's ",(0,i.jsx)(n.code,{children:"IWolverineHandler"}),". When Wolverine delivers a message it calls ",(0,i.jsx)(n.code,{children:"HandleAsync(TEvent, CancellationToken)"}),", which delegates to the registered ",(0,i.jsx)(n.code,{children:"ISubscriber"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"// Framework code \u2014 you do not write this yourself\npublic class WolverineEventHandler : IWolverineHandler\n where TEvent : class, ISerializableEvent\n{\n public async Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default)\n {\n await _subscriber.HandleAsync(@event, cancellationToken);\n }\n}\n"})}),"\n",(0,i.jsx)(n.p,{children:"This keeps application handler code free of any Wolverine API surface."}),"\n",(0,i.jsx)(n.h2,{id:"api-summary",children:"API summary"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})}),(0,i.jsxs)(n.td,{children:["Builder used with ",(0,i.jsx)(n.code,{children:"WithEventHandling"})," to configure Wolverine event handling"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IWolverineEventHandlingBuilder"})}),(0,i.jsx)(n.td,{children:"Interface for the Wolverine event handling builder"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"IEventProducer"})," that calls ",(0,i.jsx)(n.code,{children:"IMessageBus.PublishAsync"})," (fan-out)"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"SendWithWolverineEventProducer"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"IEventProducer"})," that calls ",(0,i.jsx)(n.code,{children:"IMessageBus.SendAsync"})," (point-to-point)"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"WolverineEventHandler"})}),(0,i.jsxs)(n.td,{children:["Internal ",(0,i.jsx)(n.code,{children:"IWolverineHandler"})," that bridges Wolverine to ",(0,i.jsx)(n.code,{children:"ISubscriber"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IWolverineEventHandler"})}),(0,i.jsx)(n.td,{children:"Marker interface for Wolverine event handlers"})]})]})]})]})}function u(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},75783(e,n,r){r.d(n,{A:()=>a});var i=r(30758);const t="container_xjrG",s="label_Y4p8",o="commandRow_FY5I",d="command_m7Qs",l="copyButton_u1GK";var c=r(86070);function a({packageName:e,version:n}){const[r,a]=(0,i.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,c.jsxs)("div",{className:t,children:[(0,c.jsx)("div",{className:s,children:"NuGet Package"}),(0,c.jsxs)("div",{className:o,children:[(0,c.jsx)("code",{className:d,children:h}),(0,c.jsx)("button",{className:l,onClick:()=>{navigator.clipboard.writeText(h),a(!0),setTimeout(()=>a(!1),2e3)},title:"Copy to clipboard",children:r?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,r){r.d(n,{R:()=>o,x:()=>d});var i=r(30758);const t={},s=i.createContext(t);function o(e){const n=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/e0146154.a6aea184.js b/website/build/assets/js/e0146154.a6aea184.js deleted file mode 100644 index 5713c287..00000000 --- a/website/build/assets/js/e0146154.a6aea184.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[139],{19306(e,n,r){r.r(n),r.d(n,{assets:()=>a,contentTitle:()=>d,default:()=>v,frontMatter:()=>o,metadata:()=>l,toc:()=>c});var i=r(86070),t=r(81753),s=r(75783);const o={title:"Wolverine",sidebar_position:7,description:"Use Wolverine as an event transport in RCommon's event handling pipeline, registering ISubscriber handlers and choosing between fan-out publish or point-to-point send."},d="Wolverine",l={id:"event-handling/wolverine",title:"Wolverine",description:"Use Wolverine as an event transport in RCommon's event handling pipeline, registering ISubscriber handlers and choosing between fan-out publish or point-to-point send.",source:"@site/docs/event-handling/wolverine.mdx",sourceDirName:"event-handling",slug:"/event-handling/wolverine",permalink:"/docs/next/event-handling/wolverine",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/event-handling/wolverine.mdx",tags:[],version:"current",sidebarPosition:7,frontMatter:{title:"Wolverine",sidebar_position:7,description:"Use Wolverine as an event transport in RCommon's event handling pipeline, registering ISubscriber handlers and choosing between fan-out publish or point-to-point send."},sidebar:"docsSidebar",previous:{title:"MassTransit",permalink:"/docs/next/event-handling/masstransit"},next:{title:"Messaging",permalink:"/docs/next/category/messaging"}},a={},c=[{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Defining an event",id:"defining-an-event",level:2},{value:"Implementing a subscriber",id:"implementing-a-subscriber",level:2},{value:"Publishing events",id:"publishing-events",level:2},{value:"Publish vs Send",id:"publish-vs-send",level:2},{value:"Transactional outbox",id:"transactional-outbox",level:2},{value:"How the handler bridge works",id:"how-the-handler-bridge-works",level:2},{value:"API summary",id:"api-summary",level:2}];function h(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"wolverine",children:"Wolverine"}),"\n",(0,i.jsxs)(n.p,{children:["RCommon integrates with Wolverine to deliver events both in-process and across service boundaries. The integration registers ",(0,i.jsx)(n.code,{children:"WolverineEventHandler"})," as a Wolverine ",(0,i.jsx)(n.code,{children:"IWolverineHandler"})," that delegates to the application's ",(0,i.jsx)(n.code,{children:"ISubscriber"}),". Application handler code has no dependency on Wolverine types."]}),"\n",(0,i.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,i.jsx)(s.A,{packageName:"RCommon.Wolverine"}),"\n",(0,i.jsxs)(n.p,{children:["Wolverine requires host-level registration via ",(0,i.jsx)(n.code,{children:"UseWolverine()"})," in addition to the RCommon service configuration:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"builder.Host.UseWolverine(opts =>\r\n{\r\n // Wolverine transport and endpoint options\r\n});\n"})}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["Use ",(0,i.jsx)(n.code,{children:"WithEventHandling"})," on the RCommon builder:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon;\r\nusing RCommon.Wolverine;\r\nusing RCommon.Wolverine.Producers;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithEventHandling(wlv =>\r\n {\r\n // Register the producer that publishes events via Wolverine\r\n wlv.AddProducer();\r\n\r\n // Register subscribers and record routing information\r\n wlv.AddSubscriber();\r\n wlv.AddSubscriber();\r\n });\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"AddSubscriber"})," registers ",(0,i.jsx)(n.code,{children:"ISubscriber"})," as scoped in the DI container and records the event-to-producer association in the ",(0,i.jsx)(n.code,{children:"EventSubscriptionManager"})," so the router routes this event only to Wolverine producers."]}),"\n",(0,i.jsx)(n.h2,{id:"defining-an-event",children:"Defining an event"}),"\n",(0,i.jsxs)(n.p,{children:["Events must implement ",(0,i.jsx)(n.code,{children:"ISerializableEvent"}),". Include a parameterless constructor for Wolverine deserialization:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"using RCommon.Models.Events;\r\n\r\npublic class OrderShipped : ISyncEvent\r\n{\r\n public OrderShipped(Guid orderId, string trackingNumber)\r\n {\r\n OrderId = orderId;\r\n TrackingNumber = trackingNumber;\r\n }\r\n\r\n public OrderShipped() { }\r\n\r\n public Guid OrderId { get; }\r\n public string TrackingNumber { get; } = string.Empty;\r\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"implementing-a-subscriber",children:"Implementing a subscriber"}),"\n",(0,i.jsxs)(n.p,{children:["Implement ",(0,i.jsx)(n.code,{children:"ISubscriber"}),". No Wolverine types appear in handler code:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'using RCommon.EventHandling.Subscribers;\r\n\r\npublic class OrderShippedHandler : ISubscriber\r\n{\r\n private readonly ILogger _logger;\r\n\r\n public OrderShippedHandler(ILogger logger)\r\n {\r\n _logger = logger;\r\n }\r\n\r\n public async Task HandleAsync(OrderShipped @event, CancellationToken cancellationToken = default)\r\n {\r\n _logger.LogInformation(\r\n "Order {OrderId} shipped with tracking {TrackingNumber}",\r\n @event.OrderId, @event.TrackingNumber);\r\n await Task.CompletedTask;\r\n }\r\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"publishing-events",children:"Publishing events"}),"\n",(0,i.jsxs)(n.p,{children:["Call ",(0,i.jsx)(n.code,{children:"IEventRouter.RouteEventsAsync"})," after queuing events. The router forwards each event to ",(0,i.jsx)(n.code,{children:"PublishWithWolverineEventProducer"}),", which calls ",(0,i.jsx)(n.code,{children:"IMessageBus.PublishAsync"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class ShippingService\r\n{\r\n private readonly IEventRouter _eventRouter;\r\n\r\n public ShippingService(IEventRouter eventRouter)\r\n {\r\n _eventRouter = eventRouter;\r\n }\r\n\r\n public async Task ShipOrderAsync(Guid orderId, string trackingNumber, CancellationToken cancellationToken)\r\n {\r\n // ... update order state ...\r\n\r\n _eventRouter.AddTransactionalEvent(new OrderShipped(orderId, trackingNumber));\r\n await _eventRouter.RouteEventsAsync(cancellationToken);\r\n }\r\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"publish-vs-send",children:"Publish vs Send"}),"\n",(0,i.jsxs)(n.p,{children:["Register ",(0,i.jsx)(n.code,{children:"SendWithWolverineEventProducer"})," instead of (or in addition to) ",(0,i.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})," when you want point-to-point delivery to a single handler endpoint:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"wlv.AddProducer();\n"})}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"SendWithWolverineEventProducer"})," calls ",(0,i.jsx)(n.code,{children:"IMessageBus.SendAsync"})," internally."]}),"\n",(0,i.jsx)(n.h2,{id:"transactional-outbox",children:"Transactional outbox"}),"\n",(0,i.jsxs)(n.p,{children:["Pair Wolverine with the outbox pattern to guarantee at-least-once delivery. See ",(0,i.jsx)(n.a,{href:"/docs/next/event-handling/transactional-outbox#wolverine-outbox",children:"Transactional Outbox"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"how-the-handler-bridge-works",children:"How the handler bridge works"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"WolverineEventHandler"})," implements both ",(0,i.jsx)(n.code,{children:"IWolverineEventHandler"})," and Wolverine's ",(0,i.jsx)(n.code,{children:"IWolverineHandler"}),". When Wolverine delivers a message it calls ",(0,i.jsx)(n.code,{children:"HandleAsync(TEvent, CancellationToken)"}),", which delegates to the registered ",(0,i.jsx)(n.code,{children:"ISubscriber"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"// Framework code \u2014 you do not write this yourself\r\npublic class WolverineEventHandler : IWolverineHandler\r\n where TEvent : class, ISerializableEvent\r\n{\r\n public async Task HandleAsync(TEvent @event, CancellationToken cancellationToken = default)\r\n {\r\n await _subscriber.HandleAsync(@event, cancellationToken);\r\n }\r\n}\n"})}),"\n",(0,i.jsx)(n.p,{children:"This keeps application handler code free of any Wolverine API surface."}),"\n",(0,i.jsx)(n.h2,{id:"api-summary",children:"API summary"}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Type"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"WolverineEventHandlingBuilder"})}),(0,i.jsxs)(n.td,{children:["Builder used with ",(0,i.jsx)(n.code,{children:"WithEventHandling"})," to configure Wolverine event handling"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IWolverineEventHandlingBuilder"})}),(0,i.jsx)(n.td,{children:"Interface for the Wolverine event handling builder"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"PublishWithWolverineEventProducer"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"IEventProducer"})," that calls ",(0,i.jsx)(n.code,{children:"IMessageBus.PublishAsync"})," (fan-out)"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"SendWithWolverineEventProducer"})}),(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"IEventProducer"})," that calls ",(0,i.jsx)(n.code,{children:"IMessageBus.SendAsync"})," (point-to-point)"]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"WolverineEventHandler"})}),(0,i.jsxs)(n.td,{children:["Internal ",(0,i.jsx)(n.code,{children:"IWolverineHandler"})," that bridges Wolverine to ",(0,i.jsx)(n.code,{children:"ISubscriber"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IWolverineEventHandler"})}),(0,i.jsx)(n.td,{children:"Marker interface for Wolverine event handlers"})]})]})]})]})}function v(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},75783(e,n,r){r.d(n,{A:()=>c});var i=r(30758);const t="container_xjrG",s="label_Y4p8",o="commandRow_FY5I",d="command_m7Qs",l="copyButton_u1GK";var a=r(86070);function c({packageName:e,version:n}){const[r,c]=(0,i.useState)(!1),h=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,a.jsxs)("div",{className:t,children:[(0,a.jsx)("div",{className:s,children:"NuGet Package"}),(0,a.jsxs)("div",{className:o,children:[(0,a.jsx)("code",{className:d,children:h}),(0,a.jsx)("button",{className:l,onClick:()=>{navigator.clipboard.writeText(h),c(!0),setTimeout(()=>c(!1),2e3)},title:"Copy to clipboard",children:r?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,r){r.d(n,{R:()=>o,x:()=>d});var i=r(30758);const t={},s=i.createContext(t);function o(e){const n=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),i.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/e3ba908e.02341014.js b/website/build/assets/js/e3ba908e.02341014.js new file mode 100644 index 00000000..bbba2485 --- /dev/null +++ b/website/build/assets/js/e3ba908e.02341014.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[7134],{60485(e,n,i){i.r(n),i.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>h,frontMatter:()=>t,metadata:()=>d,toc:()=>l});var s=i(86070),r=i(81753);const t={title:"Modular Composition",sidebar_position:2,description:"Compose AddRCommon across multiple modules in a single process \u2014 registrations merge, never duplicate or throw on agreement."},o="Modular Composition",d={id:"core-concepts/modular-composition",title:"Modular Composition",description:"Compose AddRCommon across multiple modules in a single process \u2014 registrations merge, never duplicate or throw on agreement.",source:"@site/docs/core-concepts/modular-composition.mdx",sourceDirName:"core-concepts",slug:"/core-concepts/modular-composition",permalink:"/docs/next/core-concepts/modular-composition",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/core-concepts/modular-composition.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"Modular Composition",sidebar_position:2,description:"Compose AddRCommon across multiple modules in a single process \u2014 registrations merge, never duplicate or throw on agreement."},sidebar:"docsSidebar",previous:{title:"Fluent Configuration",permalink:"/docs/next/core-concepts/fluent-configuration"},next:{title:"Guards & Validation",permalink:"/docs/next/core-concepts/guards"}},c={},l=[{value:"Overview",id:"overview",level:2},{value:"The problem",id:"the-problem",level:2},{value:"The solution",id:"the-solution",level:2},{value:"A two-module example",id:"a-two-module-example",level:2},{value:"Conflict semantics",id:"conflict-semantics",level:2},{value:"New helper APIs",id:"new-helper-apis",level:2},{value:"IServiceCollection.IsRCommonInitialized()",id:"iservicecollectionisrcommoninitialized",level:3},{value:"IRCommonBuilder.GetOrAddBuilder<TSubBuilder>(Func<TSubBuilder>)",id:"ircommonbuildergetoraddbuildertsubbuilderfunctsubbuilder",level:3},{value:"IRCommonBuilder.GetBootstrapDiagnostics()",id:"ircommonbuildergetbootstrapdiagnostics",level:3},{value:"Diagnostics at startup",id:"diagnostics-at-startup",level:2},{value:"What's NOT thread-safe",id:"whats-not-thread-safe",level:2},{value:"See also",id:"see-also",level:2}];function a(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,r.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"modular-composition",children:"Modular Composition"}),"\n",(0,s.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,s.jsxs)(n.p,{children:["RCommon's bootstrapper supports modular composition: any number of modules in the same process can independently call ",(0,s.jsx)(n.code,{children:"services.AddRCommon().WithX(...)"})," and the registrations merge predictably. Sub-builders are cached by concrete type, singleton-style verbs are idempotent on same-type re-registration, and conflicts (different impls competing for the same slot) throw with a diagnostic message that names both types."]}),"\n",(0,s.jsxs)(n.p,{children:["This page documents the contract. For the underlying builder API itself, see ",(0,s.jsx)(n.a,{href:"/docs/next/core-concepts/fluent-configuration",children:"Fluent Configuration"}),"."]}),"\n",(0,s.jsx)(n.h2,{id:"the-problem",children:"The problem"}),"\n",(0,s.jsxs)(n.p,{children:["Before modular composition, ",(0,s.jsx)(n.code,{children:"AddRCommon()"})," assumed a single composition root. Applications that organized startup around feature-folders or per-module wiring could not safely invoke ",(0,s.jsx)(n.code,{children:"AddRCommon()"})," from more than one place:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["Calling ",(0,s.jsx)(n.code,{children:"AddRCommon()"})," twice double-registered ",(0,s.jsx)(n.code,{children:"IEventBus"}),", ",(0,s.jsx)(n.code,{children:"EventSubscriptionManager"}),", and ",(0,s.jsx)(n.code,{children:"IEventRouter"}),"."]}),"\n",(0,s.jsx)(n.li,{children:"Internal guard flags on the builder reset on the second call, masking subsequent conflicts."}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"DataStoreFactoryOptions"})," accepted duplicate ",(0,s.jsx)(n.code,{children:"(name, base)"})," mappings and silently overwrote earlier ones."]}),"\n",(0,s.jsxs)(n.li,{children:["Singleton-style verbs such as ",(0,s.jsx)(n.code,{children:"WithSimpleGuidGenerator"})," and ",(0,s.jsx)(n.code,{children:"WithDateTimeSystem"})," threw on every second call \u2014 even when the second call agreed with the first."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"The net effect: modular apps had to funnel all RCommon wiring through a single static method, which defeated the purpose of modular composition."}),"\n",(0,s.jsx)(n.h2,{id:"the-solution",children:"The solution"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"AddRCommon()"})," is now idempotent. The first call constructs an ",(0,s.jsx)(n.code,{children:"IRCommonBuilder"})," and stashes it on ",(0,s.jsx)(n.code,{children:"IServiceCollection"}),"; subsequent calls return the same cached instance. Sub-builders are cached by concrete builder type, so a second ",(0,s.jsx)(n.code,{children:"WithPersistence(ef => ...)"})," reuses the same ",(0,s.jsx)(n.code,{children:"EFCorePerisistenceBuilder"})," and the new action delegate runs against it. Singleton-style verbs record the impl type they registered; same-type re-registration is a no-op, different-type calls throw ",(0,s.jsx)(n.code,{children:"RCommonBuilderException"})," with both type names in the message."]}),"\n",(0,s.jsx)(n.p,{children:"Conflict semantics are predictable across the entire surface \u2014 see the matrix below."}),"\n",(0,s.jsx)(n.h2,{id:"a-two-module-example",children:"A two-module example"}),"\n",(0,s.jsx)(n.p,{children:"Define a minimal module contract and let each module configure RCommon independently:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'public interface IServiceModule\n{\n void Configure(IServiceCollection services);\n}\n\npublic sealed class OrderingModule : IServiceModule\n{\n public void Configure(IServiceCollection services)\n {\n services.AddRCommon()\n .WithSimpleGuidGenerator()\n .WithPersistence(ef =>\n ef.AddDbContext(\n "Ordering",\n o => o.UseInMemoryDatabase("ordering")))\n .WithEventHandling(eh =>\n eh.AddProducer());\n }\n}\n\npublic sealed class InventoryModule : IServiceModule\n{\n public void Configure(IServiceCollection services)\n {\n services.AddRCommon()\n .WithSimpleGuidGenerator() // Same impl as Ordering \u2014 idempotent no-op.\n .WithPersistence(ef =>\n ef.AddDbContext(\n "Inventory",\n o => o.UseInMemoryDatabase("inventory")))\n .WithMemoryCaching();\n }\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Compose them inside ",(0,s.jsx)(n.code,{children:"Host.CreateDefaultBuilder"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"var host = Host.CreateDefaultBuilder(args)\n .ConfigureServices(services =>\n {\n IServiceModule[] modules =\n {\n new OrderingModule(),\n new InventoryModule(),\n };\n\n foreach (var module in modules)\n {\n module.Configure(services);\n }\n })\n .Build();\n"})}),"\n",(0,s.jsx)(n.p,{children:"What happens at runtime:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"AddRCommon()"})," runs twice but returns the same builder on the second call."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"WithSimpleGuidGenerator()"})," runs twice; the second call detects the impl is already registered and is a no-op."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"WithPersistence"})," runs twice; the same persistence sub-builder is reused and ",(0,s.jsx)(n.code,{children:"AddDbContext"})," accumulates both ",(0,s.jsx)(n.code,{children:'"Ordering"'})," and ",(0,s.jsx)(n.code,{children:'"Inventory"'})," data stores."]}),"\n",(0,s.jsx)(n.li,{children:"Only the modules that opt in to event handling and caching pay the cost."}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["A runnable three-module version of this example \u2014 including a producer registered twice and deduplicated \u2014 lives under ",(0,s.jsx)(n.code,{children:"Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/"}),"."]}),"\n",(0,s.jsx)(n.h2,{id:"conflict-semantics",children:"Conflict semantics"}),"\n",(0,s.jsx)(n.p,{children:"The table below documents every verb category and what happens when it is called more than once."}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Verb category"}),(0,s.jsx)(n.th,{children:"Same impl re-registered"}),(0,s.jsx)(n.th,{children:"Different impl"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"AddRCommon()"})}),(0,s.jsxs)(n.td,{children:["Returns the cached ",(0,s.jsx)(n.code,{children:"IRCommonBuilder"}),"."]}),(0,s.jsx)(n.td,{children:"n/a"})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.td,{children:["Sub-builder verbs (",(0,s.jsx)(n.code,{children:"WithPersistence"}),", ",(0,s.jsx)(n.code,{children:"WithMediator"}),", ",(0,s.jsx)(n.code,{children:"WithEventHandling"}),", ",(0,s.jsx)(n.code,{children:"WithCQRS"}),", ",(0,s.jsx)(n.code,{children:"WithValidation"}),", ",(0,s.jsx)(n.code,{children:"WithBlobStorage"}),", ",(0,s.jsx)(n.code,{children:"WithMultiTenancy"}),", ",(0,s.jsx)(n.code,{children:"WithUnitOfWork"}),", ",(0,s.jsx)(n.code,{children:"WithMemoryCaching"}),", ",(0,s.jsx)(n.code,{children:"WithDistributedCaching"}),")"]}),(0,s.jsxs)(n.td,{children:["Cached sub-builder is reused; the supplied ",(0,s.jsx)(n.code,{children:"Action"})," runs against the same instance, so registrations accumulate."]}),(0,s.jsxs)(n.td,{children:["Both sub-builders register side by side (e.g. ",(0,s.jsx)(n.code,{children:"WithPersistence"})," + ",(0,s.jsx)(n.code,{children:"WithPersistence"}),")."]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.td,{children:["Singleton-style verbs (",(0,s.jsx)(n.code,{children:"WithSimpleGuidGenerator"}),", ",(0,s.jsx)(n.code,{children:"WithSequentialGuidGenerator"}),", ",(0,s.jsx)(n.code,{children:"WithDateTimeSystem"}),", ",(0,s.jsx)(n.code,{children:"WithJsonSerialization"}),", ",(0,s.jsx)(n.code,{children:"WithSmtpEmailServices"}),", ",(0,s.jsx)(n.code,{children:"WithSendGridEmailServices"}),")"]}),(0,s.jsx)(n.td,{children:"Idempotent no-op."}),(0,s.jsxs)(n.td,{children:["Throws ",(0,s.jsx)(n.code,{children:"RCommonBuilderException"})," with both impl type names and a remediation hint."]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"AddDbContext(name, ...)"})}),(0,s.jsxs)(n.td,{children:["Idempotent for the same ",(0,s.jsx)(n.code,{children:"(name, TDbContext)"})," pair."]}),(0,s.jsxs)(n.td,{children:["Throws ",(0,s.jsx)(n.code,{children:"UnsupportedDataStoreException"})," when the same ",(0,s.jsx)(n.code,{children:"name"})," is re-bound to a different ",(0,s.jsx)(n.code,{children:"TDbContext"}),"."]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"AddProducer"})}),(0,s.jsxs)(n.td,{children:["Same ",(0,s.jsx)(n.code,{children:"T"})," is registered exactly once via descriptor scan."]}),(0,s.jsxs)(n.td,{children:["Distinct ",(0,s.jsx)(n.code,{children:"T"})," types coexist."]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsxs)(n.td,{children:["Parameterless verbs (",(0,s.jsx)(n.code,{children:"WithStatelessStateMachine"}),", ",(0,s.jsx)(n.code,{children:"WithMassTransitStateMachine"}),", ",(0,s.jsx)(n.code,{children:"WithClaimsAndPrincipalAccessor"}),")"]}),(0,s.jsxs)(n.td,{children:[(0,s.jsx)(n.code,{children:"TryAdd"}),"-hardened \u2014 idempotent."]}),(0,s.jsx)(n.td,{children:"n/a"})]})]})]}),"\n",(0,s.jsx)(n.p,{children:'The "Same impl" column is what makes modular composition usable: modules can independently declare their requirements without coordinating with other modules.'}),"\n",(0,s.jsx)(n.h2,{id:"new-helper-apis",children:"New helper APIs"}),"\n",(0,s.jsx)(n.h3,{id:"iservicecollectionisrcommoninitialized",children:(0,s.jsx)(n.code,{children:"IServiceCollection.IsRCommonInitialized()"})}),"\n",(0,s.jsxs)(n.p,{children:["Returns ",(0,s.jsx)(n.code,{children:"true"})," if any module in the process has already called ",(0,s.jsx)(n.code,{children:"AddRCommon()"}),". Useful when a module's behavior depends on whether RCommon is in play:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"if (!services.IsRCommonInitialized())\n{\n // First module to wire RCommon \u2014 set up shared infrastructure once.\n}\nservices.AddRCommon().WithSimpleGuidGenerator();\n"})}),"\n",(0,s.jsxs)(n.p,{children:["You rarely need to call this guard in normal usage because ",(0,s.jsx)(n.code,{children:"AddRCommon()"})," is idempotent. It is most valuable in libraries that want to skip optional wiring entirely when RCommon is absent."]}),"\n",(0,s.jsx)(n.h3,{id:"ircommonbuildergetoraddbuildertsubbuilderfunctsubbuilder",children:(0,s.jsx)(n.code,{children:"IRCommonBuilder.GetOrAddBuilder(Func)"})}),"\n",(0,s.jsxs)(n.p,{children:["The hook that third-party ",(0,s.jsx)(n.code,{children:"WithX"})," extensions use to opt into sub-builder caching. The factory is invoked at most once per ",(0,s.jsx)(n.code,{children:"TSubBuilder"})," per process:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:"public static IRCommonBuilder WithMyFeature(\n this IRCommonBuilder builder,\n Action configure)\n where TBuilder : class, IMyFeatureBuilder, new()\n{\n var sub = builder.GetOrAddBuilder(() => new TBuilder { Services = builder.Services });\n configure(sub);\n return builder;\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["When two modules call ",(0,s.jsx)(n.code,{children:"WithMyFeature(...)"}),", only the first call constructs ",(0,s.jsx)(n.code,{children:"MyBuilder"}),". The second call retrieves the cached instance and runs its ",(0,s.jsx)(n.code,{children:"configure"})," delegate against it."]}),"\n",(0,s.jsx)(n.h3,{id:"ircommonbuildergetbootstrapdiagnostics",children:(0,s.jsx)(n.code,{children:"IRCommonBuilder.GetBootstrapDiagnostics()"})}),"\n",(0,s.jsxs)(n.p,{children:["After the host starts, an internal ",(0,s.jsx)(n.code,{children:"IHostedService"})," scans the registered descriptors for soft duplicates and stashes a human-readable report on the builder. Retrieve it like this:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'var host = builder.Build();\nawait host.StartAsync();\n\nvar rcommon = host.Services.GetRequiredService();\nvar report = rcommon.GetBootstrapDiagnostics();\nif (!string.IsNullOrEmpty(report))\n{\n Console.WriteLine("Bootstrap diagnostics:");\n Console.WriteLine(report);\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["When the descriptor scan finds nothing suspicious, ",(0,s.jsx)(n.code,{children:"GetBootstrapDiagnostics()"})," returns ",(0,s.jsx)(n.code,{children:"string.Empty"}),". The same report is also emitted as a single warning via ",(0,s.jsx)(n.code,{children:"ILoggerFactory"})," during host startup."]}),"\n",(0,s.jsx)(n.h2,{id:"diagnostics-at-startup",children:"Diagnostics at startup"}),"\n",(0,s.jsxs)(n.p,{children:["The duplicate-descriptor scanner runs once when the host starts. It is registered automatically by ",(0,s.jsx)(n.code,{children:"AddRCommon()"})," as a hosted service and:"]}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["Walks ",(0,s.jsx)(n.code,{children:"IServiceCollection"})," after all modules have configured services."]}),"\n",(0,s.jsxs)(n.li,{children:["Groups descriptors by ",(0,s.jsx)(n.code,{children:"(ServiceType, ImplementationType, Lifetime)"}),"."]}),"\n",(0,s.jsxs)(n.li,{children:["Emits a single warning through ",(0,s.jsx)(n.code,{children:"ILoggerFactory"})," when duplicates are present."]}),"\n",(0,s.jsxs)(n.li,{children:["Stashes the same report on the builder so application code can read it via ",(0,s.jsx)(n.code,{children:"GetBootstrapDiagnostics()"}),"."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:['"Soft duplicates" are descriptors that share ',(0,s.jsx)(n.code,{children:"ServiceType"})," + ",(0,s.jsx)(n.code,{children:"ImplementationType"})," but were not deduped automatically \u2014 typically a sign that a custom extension is bypassing the cache. Hardened built-in verbs do not produce soft duplicates."]}),"\n",(0,s.jsx)(n.h2,{id:"whats-not-thread-safe",children:"What's NOT thread-safe"}),"\n",(0,s.jsxs)(n.p,{children:["Bootstrap is single-threaded by contract. The cached builder and its sub-builders are not protected by locks. Modules must run their ",(0,s.jsx)(n.code,{children:"Configure(IServiceCollection)"})," calls serially, exactly as ",(0,s.jsx)(n.code,{children:"Host.CreateDefaultBuilder(args).ConfigureServices(...)"})," does. Once ",(0,s.jsx)(n.code,{children:"host.Build()"})," returns, the service provider is fully constructed and standard DI thread-safety applies."]}),"\n",(0,s.jsx)(n.p,{children:"If you parallelize module wiring you will see intermittent missing registrations and lost configuration actions. Don't."}),"\n",(0,s.jsx)(n.h2,{id:"see-also",children:"See also"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:"Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/"})," \u2014 a runnable three-module demonstration including producer deduplication and the diagnostic report."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"/docs/next/core-concepts/fluent-configuration",children:"Fluent Configuration"})," \u2014 the underlying builder API."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"/docs/next/core-concepts/guid-generation",children:"GUID Generation"})," \u2014 singleton-style verb that participates in the conflict matrix."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.a,{href:"/docs/next/core-concepts/system-time",children:"System Time"})," \u2014 same."]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(a,{...e})}):a(e)}},81753(e,n,i){i.d(n,{R:()=>o,x:()=>d});var s=i(30758);const r={},t=s.createContext(r);function o(e){const n=s.useContext(t);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),s.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/e59bb016.4b2d39b4.js b/website/build/assets/js/e59bb016.4b2d39b4.js new file mode 100644 index 00000000..b944ca26 --- /dev/null +++ b/website/build/assets/js/e59bb016.4b2d39b4.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[8599],{14897(e,i,n){n.r(i),n.d(i,{assets:()=>l,contentTitle:()=>o,default:()=>h,frontMatter:()=>d,metadata:()=>r,toc:()=>a});var t=n(86070),s=n(81753),c=n(75783);const d={title:"System Time Abstraction",sidebar_position:4,description:"Use ISystemTime to abstract DateTime.Now for testable time-dependent code \u2014 configure UTC or local time, normalize incoming dates, and substitute clocks in tests."},o="System Time Abstraction",r={id:"core-concepts/system-time",title:"System Time Abstraction",description:"Use ISystemTime to abstract DateTime.Now for testable time-dependent code \u2014 configure UTC or local time, normalize incoming dates, and substitute clocks in tests.",source:"@site/docs/core-concepts/system-time.mdx",sourceDirName:"core-concepts",slug:"/core-concepts/system-time",permalink:"/docs/next/core-concepts/system-time",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/core-concepts/system-time.mdx",tags:[],version:"current",sidebarPosition:4,frontMatter:{title:"System Time Abstraction",sidebar_position:4,description:"Use ISystemTime to abstract DateTime.Now for testable time-dependent code \u2014 configure UTC or local time, normalize incoming dates, and substitute clocks in tests."},sidebar:"docsSidebar",previous:{title:"GUID Generation",permalink:"/docs/next/core-concepts/guid-generation"},next:{title:"Execution Results & Models",permalink:"/docs/next/core-concepts/execution-results"}},l={},a=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"UTC time (recommended for multi-timezone applications)",id:"utc-time-recommended-for-multi-timezone-applications",level:3},{value:"Local server time",id:"local-server-time",level:3},{value:"Unspecified (default)",id:"unspecified-default",level:3},{value:"Usage",id:"usage",level:2},{value:"Injecting ISystemTime in production code",id:"injecting-isystemtime-in-production-code",level:3},{value:"Normalizing incoming DateTime values",id:"normalizing-incoming-datetime-values",level:3},{value:"In unit tests",id:"in-unit-tests",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"ISystemTime",id:"isystemtime",level:3},{value:"SystemTime (default implementation)",id:"systemtime-default-implementation",level:3},{value:"SystemTimeOptions",id:"systemtimeoptions",level:3}];function m(e){const i={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(i.h1,{id:"system-time-abstraction",children:"System Time Abstraction"}),"\n",(0,t.jsx)(i.h2,{id:"overview",children:"Overview"}),"\n",(0,t.jsxs)(i.p,{children:["Code that calls ",(0,t.jsx)(i.code,{children:"DateTime.Now"})," or ",(0,t.jsx)(i.code,{children:"DateTime.UtcNow"})," directly is difficult to test because the value changes with wall-clock time. RCommon provides an ",(0,t.jsx)(i.code,{children:"ISystemTime"})," abstraction that wraps the system clock. Injecting ",(0,t.jsx)(i.code,{children:"ISystemTime"})," instead of reading the clock directly lets you substitute a fixed time in unit tests without modifying production code."]}),"\n",(0,t.jsxs)(i.p,{children:["Beyond testability, ",(0,t.jsx)(i.code,{children:"ISystemTime"})," also centralizes time zone handling. When ",(0,t.jsx)(i.code,{children:"DateTimeKind.Utc"})," is configured, ",(0,t.jsx)(i.code,{children:"ISystemTime.Now"})," always returns UTC, and the ",(0,t.jsx)(i.code,{children:"Normalize"})," method converts any incoming ",(0,t.jsx)(i.code,{children:"DateTime"})," to match that kind."]}),"\n",(0,t.jsx)(i.h2,{id:"installation",children:"Installation"}),"\n",(0,t.jsx)(c.A,{packageName:"RCommon.Core"}),"\n",(0,t.jsx)(i.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsxs)(i.p,{children:["Configure the system time inside the ",(0,t.jsx)(i.code,{children:"AddRCommon()"})," builder chain."]}),"\n",(0,t.jsx)(i.h3,{id:"utc-time-recommended-for-multi-timezone-applications",children:"UTC time (recommended for multi-timezone applications)"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithDateTimeSystem(options =>\n {\n options.Kind = DateTimeKind.Utc;\n });\n"})}),"\n",(0,t.jsx)(i.h3,{id:"local-server-time",children:"Local server time"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithDateTimeSystem(options =>\n {\n options.Kind = DateTimeKind.Local;\n });\n"})}),"\n",(0,t.jsx)(i.h3,{id:"unspecified-default",children:"Unspecified (default)"}),"\n",(0,t.jsxs)(i.p,{children:["When ",(0,t.jsx)(i.code,{children:"Kind"})," is ",(0,t.jsx)(i.code,{children:"DateTimeKind.Unspecified"}),", ",(0,t.jsx)(i.code,{children:"ISystemTime.Now"})," returns ",(0,t.jsx)(i.code,{children:"DateTime.Now"})," and no normalization conversions are applied."]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\n .WithDateTimeSystem(options =>\n {\n options.Kind = DateTimeKind.Unspecified;\n });\n"})}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"WithDateTimeSystem"})," is now idempotent \u2014 calling it multiple times is safe and any options actions you supply accumulate via the Options pattern. This lets multiple modules independently configure the date/time system without coordination. See ",(0,t.jsx)(i.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix across all ",(0,t.jsx)(i.code,{children:"With*"})," verbs."]}),"\n",(0,t.jsx)(i.h2,{id:"usage",children:"Usage"}),"\n",(0,t.jsx)(i.h3,{id:"injecting-isystemtime-in-production-code",children:"Injecting ISystemTime in production code"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-csharp",children:"using RCommon;\n\npublic class AuditService\n{\n private readonly ISystemTime _systemTime;\n private readonly IAuditRepository _repository;\n\n public AuditService(ISystemTime systemTime, IAuditRepository repository)\n {\n _systemTime = systemTime;\n _repository = repository;\n }\n\n public async Task RecordEvent(string description)\n {\n var entry = new AuditEntry\n {\n OccurredAt = _systemTime.Now,\n Description = description\n };\n await _repository.AddAsync(entry);\n }\n}\n"})}),"\n",(0,t.jsx)(i.h3,{id:"normalizing-incoming-datetime-values",children:"Normalizing incoming DateTime values"}),"\n",(0,t.jsxs)(i.p,{children:["Use ",(0,t.jsx)(i.code,{children:"Normalize"})," when accepting a ",(0,t.jsx)(i.code,{children:"DateTime"})," from an external source (e.g., an API request) and you need it to conform to the application's configured ",(0,t.jsx)(i.code,{children:"DateTimeKind"}),":"]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-csharp",children:"public void SetScheduledDate(DateTime scheduledDate)\n{\n // Converts from UTC to Local, or Local to UTC, depending on configured Kind.\n var normalized = _systemTime.Normalize(scheduledDate);\n _scheduledDate = normalized;\n}\n"})}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"Normalize"})," rules:"]}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:["If the configured ",(0,t.jsx)(i.code,{children:"Kind"})," is ",(0,t.jsx)(i.code,{children:"Unspecified"}),", the input is returned unchanged."]}),"\n",(0,t.jsxs)(i.li,{children:["If the configured ",(0,t.jsx)(i.code,{children:"Kind"})," matches the input's ",(0,t.jsx)(i.code,{children:"Kind"}),", no conversion occurs."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.code,{children:"Local"})," + UTC input: converts to local time via ",(0,t.jsx)(i.code,{children:"ToLocalTime()"}),"."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.code,{children:"Utc"})," + Local input: converts to UTC via ",(0,t.jsx)(i.code,{children:"ToUniversalTime()"}),"."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.code,{children:"Unspecified"})," input: re-specifies the ",(0,t.jsx)(i.code,{children:"Kind"})," without converting the value."]}),"\n"]}),"\n",(0,t.jsx)(i.h3,{id:"in-unit-tests",children:"In unit tests"}),"\n",(0,t.jsxs)(i.p,{children:["Substitute ",(0,t.jsx)(i.code,{children:"ISystemTime"})," with a test double that returns a known timestamp:"]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-csharp",children:'public class FakeSystemTime : ISystemTime\n{\n private readonly DateTime _fixedTime;\n\n public FakeSystemTime(DateTime fixedTime)\n {\n _fixedTime = fixedTime;\n }\n\n public DateTime Now => _fixedTime;\n public DateTimeKind Kind => _fixedTime.Kind;\n public bool SupportsMultipleTimezone => _fixedTime.Kind == DateTimeKind.Utc;\n public DateTime Normalize(DateTime dateTime) => dateTime;\n}\n\n// In a test\nvar fixedTime = new DateTime(2026, 1, 15, 9, 0, 0, DateTimeKind.Utc);\nvar fakeTime = new FakeSystemTime(fixedTime);\nvar service = new AuditService(fakeTime, repositoryMock.Object);\n\nawait service.RecordEvent("order placed");\n\nrepositoryMock.Verify(r => r.AddAsync(It.Is(e => e.OccurredAt == fixedTime)));\n'})}),"\n",(0,t.jsx)(i.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,t.jsx)(i.h3,{id:"isystemtime",children:(0,t.jsx)(i.code,{children:"ISystemTime"})}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Member"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DateTime Now { get; }"})}),(0,t.jsxs)(i.td,{children:["Returns the current date and time according to the configured ",(0,t.jsx)(i.code,{children:"Kind"}),"."]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DateTimeKind Kind { get; }"})}),(0,t.jsxs)(i.td,{children:["The ",(0,t.jsx)(i.code,{children:"DateTimeKind"})," this instance was configured with."]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"bool SupportsMultipleTimezone { get; }"})}),(0,t.jsxs)(i.td,{children:[(0,t.jsx)(i.code,{children:"true"})," when ",(0,t.jsx)(i.code,{children:"Kind"})," is ",(0,t.jsx)(i.code,{children:"Utc"}),", indicating the time is safe to store across time zones."]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DateTime Normalize(DateTime dateTime)"})}),(0,t.jsxs)(i.td,{children:["Converts the provided ",(0,t.jsx)(i.code,{children:"DateTime"})," to match the configured ",(0,t.jsx)(i.code,{children:"Kind"}),"."]})]})]})]}),"\n",(0,t.jsxs)(i.h3,{id:"systemtime-default-implementation",children:[(0,t.jsx)(i.code,{children:"SystemTime"})," (default implementation)"]}),"\n",(0,t.jsxs)(i.p,{children:["Registered as ",(0,t.jsx)(i.code,{children:"ISystemTime"})," by ",(0,t.jsx)(i.code,{children:"WithDateTimeSystem(...)"}),". Transient lifetime. ",(0,t.jsx)(i.code,{children:"Now"})," returns ",(0,t.jsx)(i.code,{children:"DateTime.UtcNow"})," when ",(0,t.jsx)(i.code,{children:"Kind"})," is ",(0,t.jsx)(i.code,{children:"Utc"}),", otherwise ",(0,t.jsx)(i.code,{children:"DateTime.Now"}),"."]}),"\n",(0,t.jsx)(i.h3,{id:"systemtimeoptions",children:(0,t.jsx)(i.code,{children:"SystemTimeOptions"})}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Property"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Default"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"Kind"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DateTimeKind"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DateTimeKind.Unspecified"})}),(0,t.jsx)(i.td,{children:"Controls whether the system clock operates in UTC, Local, or Unspecified mode."})]})})]})]})}function h(e={}){const{wrapper:i}={...(0,s.R)(),...e.components};return i?(0,t.jsx)(i,{...e,children:(0,t.jsx)(m,{...e})}):m(e)}},75783(e,i,n){n.d(i,{A:()=>a});var t=n(30758);const s="container_xjrG",c="label_Y4p8",d="commandRow_FY5I",o="command_m7Qs",r="copyButton_u1GK";var l=n(86070);function a({packageName:e,version:i}){const[n,a]=(0,t.useState)(!1),m=i?`dotnet add package ${e} --version ${i}`:`dotnet add package ${e}`;return(0,l.jsxs)("div",{className:s,children:[(0,l.jsx)("div",{className:c,children:"NuGet Package"}),(0,l.jsxs)("div",{className:d,children:[(0,l.jsx)("code",{className:o,children:m}),(0,l.jsx)("button",{className:r,onClick:()=>{navigator.clipboard.writeText(m),a(!0),setTimeout(()=>a(!1),2e3)},title:"Copy to clipboard",children:n?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,i,n){n.d(i,{R:()=>d,x:()=>o});var t=n(30758);const s={},c=t.createContext(s);function d(e){const i=t.useContext(c);return t.useMemo(function(){return"function"==typeof e?e(i):{...i,...e}},[i,e])}function o(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:d(e.components),t.createElement(c.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/e59bb016.99fcea04.js b/website/build/assets/js/e59bb016.99fcea04.js deleted file mode 100644 index 8f137b52..00000000 --- a/website/build/assets/js/e59bb016.99fcea04.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[8599],{14897(e,i,n){n.r(i),n.d(i,{assets:()=>l,contentTitle:()=>d,default:()=>h,frontMatter:()=>c,metadata:()=>o,toc:()=>a});var t=n(86070),s=n(81753),r=n(75783);const c={title:"System Time Abstraction",sidebar_position:4,description:"Use ISystemTime to abstract DateTime.Now for testable time-dependent code \u2014 configure UTC or local time, normalize incoming dates, and substitute clocks in tests."},d="System Time Abstraction",o={id:"core-concepts/system-time",title:"System Time Abstraction",description:"Use ISystemTime to abstract DateTime.Now for testable time-dependent code \u2014 configure UTC or local time, normalize incoming dates, and substitute clocks in tests.",source:"@site/docs/core-concepts/system-time.mdx",sourceDirName:"core-concepts",slug:"/core-concepts/system-time",permalink:"/docs/next/core-concepts/system-time",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/core-concepts/system-time.mdx",tags:[],version:"current",sidebarPosition:4,frontMatter:{title:"System Time Abstraction",sidebar_position:4,description:"Use ISystemTime to abstract DateTime.Now for testable time-dependent code \u2014 configure UTC or local time, normalize incoming dates, and substitute clocks in tests."},sidebar:"docsSidebar",previous:{title:"GUID Generation",permalink:"/docs/next/core-concepts/guid-generation"},next:{title:"Execution Results & Models",permalink:"/docs/next/core-concepts/execution-results"}},l={},a=[{value:"Overview",id:"overview",level:2},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"UTC time (recommended for multi-timezone applications)",id:"utc-time-recommended-for-multi-timezone-applications",level:3},{value:"Local server time",id:"local-server-time",level:3},{value:"Unspecified (default)",id:"unspecified-default",level:3},{value:"Usage",id:"usage",level:2},{value:"Injecting ISystemTime in production code",id:"injecting-isystemtime-in-production-code",level:3},{value:"Normalizing incoming DateTime values",id:"normalizing-incoming-datetime-values",level:3},{value:"In unit tests",id:"in-unit-tests",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"ISystemTime",id:"isystemtime",level:3},{value:"SystemTime (default implementation)",id:"systemtime-default-implementation",level:3},{value:"SystemTimeOptions",id:"systemtimeoptions",level:3}];function m(e){const i={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(i.h1,{id:"system-time-abstraction",children:"System Time Abstraction"}),"\n",(0,t.jsx)(i.h2,{id:"overview",children:"Overview"}),"\n",(0,t.jsxs)(i.p,{children:["Code that calls ",(0,t.jsx)(i.code,{children:"DateTime.Now"})," or ",(0,t.jsx)(i.code,{children:"DateTime.UtcNow"})," directly is difficult to test because the value changes with wall-clock time. RCommon provides an ",(0,t.jsx)(i.code,{children:"ISystemTime"})," abstraction that wraps the system clock. Injecting ",(0,t.jsx)(i.code,{children:"ISystemTime"})," instead of reading the clock directly lets you substitute a fixed time in unit tests without modifying production code."]}),"\n",(0,t.jsxs)(i.p,{children:["Beyond testability, ",(0,t.jsx)(i.code,{children:"ISystemTime"})," also centralizes time zone handling. When ",(0,t.jsx)(i.code,{children:"DateTimeKind.Utc"})," is configured, ",(0,t.jsx)(i.code,{children:"ISystemTime.Now"})," always returns UTC, and the ",(0,t.jsx)(i.code,{children:"Normalize"})," method converts any incoming ",(0,t.jsx)(i.code,{children:"DateTime"})," to match that kind."]}),"\n",(0,t.jsx)(i.h2,{id:"installation",children:"Installation"}),"\n",(0,t.jsx)(r.A,{packageName:"RCommon.Core"}),"\n",(0,t.jsx)(i.h2,{id:"configuration",children:"Configuration"}),"\n",(0,t.jsxs)(i.p,{children:["Configure the system time inside the ",(0,t.jsx)(i.code,{children:"AddRCommon()"})," builder chain."]}),"\n",(0,t.jsx)(i.h3,{id:"utc-time-recommended-for-multi-timezone-applications",children:"UTC time (recommended for multi-timezone applications)"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithDateTimeSystem(options =>\r\n {\r\n options.Kind = DateTimeKind.Utc;\r\n });\n"})}),"\n",(0,t.jsx)(i.h3,{id:"local-server-time",children:"Local server time"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithDateTimeSystem(options =>\r\n {\r\n options.Kind = DateTimeKind.Local;\r\n });\n"})}),"\n",(0,t.jsx)(i.h3,{id:"unspecified-default",children:"Unspecified (default)"}),"\n",(0,t.jsxs)(i.p,{children:["When ",(0,t.jsx)(i.code,{children:"Kind"})," is ",(0,t.jsx)(i.code,{children:"DateTimeKind.Unspecified"}),", ",(0,t.jsx)(i.code,{children:"ISystemTime.Now"})," returns ",(0,t.jsx)(i.code,{children:"DateTime.Now"})," and no normalization conversions are applied."]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-csharp",children:"builder.Services.AddRCommon()\r\n .WithDateTimeSystem(options =>\r\n {\r\n options.Kind = DateTimeKind.Unspecified;\r\n });\n"})}),"\n",(0,t.jsxs)(i.p,{children:["Only one ",(0,t.jsx)(i.code,{children:"WithDateTimeSystem"})," call is permitted per application. A second call throws ",(0,t.jsx)(i.code,{children:"RCommonBuilderException"}),"."]}),"\n",(0,t.jsx)(i.h2,{id:"usage",children:"Usage"}),"\n",(0,t.jsx)(i.h3,{id:"injecting-isystemtime-in-production-code",children:"Injecting ISystemTime in production code"}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-csharp",children:"using RCommon;\r\n\r\npublic class AuditService\r\n{\r\n private readonly ISystemTime _systemTime;\r\n private readonly IAuditRepository _repository;\r\n\r\n public AuditService(ISystemTime systemTime, IAuditRepository repository)\r\n {\r\n _systemTime = systemTime;\r\n _repository = repository;\r\n }\r\n\r\n public async Task RecordEvent(string description)\r\n {\r\n var entry = new AuditEntry\r\n {\r\n OccurredAt = _systemTime.Now,\r\n Description = description\r\n };\r\n await _repository.AddAsync(entry);\r\n }\r\n}\n"})}),"\n",(0,t.jsx)(i.h3,{id:"normalizing-incoming-datetime-values",children:"Normalizing incoming DateTime values"}),"\n",(0,t.jsxs)(i.p,{children:["Use ",(0,t.jsx)(i.code,{children:"Normalize"})," when accepting a ",(0,t.jsx)(i.code,{children:"DateTime"})," from an external source (e.g., an API request) and you need it to conform to the application's configured ",(0,t.jsx)(i.code,{children:"DateTimeKind"}),":"]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-csharp",children:"public void SetScheduledDate(DateTime scheduledDate)\r\n{\r\n // Converts from UTC to Local, or Local to UTC, depending on configured Kind.\r\n var normalized = _systemTime.Normalize(scheduledDate);\r\n _scheduledDate = normalized;\r\n}\n"})}),"\n",(0,t.jsxs)(i.p,{children:[(0,t.jsx)(i.code,{children:"Normalize"})," rules:"]}),"\n",(0,t.jsxs)(i.ul,{children:["\n",(0,t.jsxs)(i.li,{children:["If the configured ",(0,t.jsx)(i.code,{children:"Kind"})," is ",(0,t.jsx)(i.code,{children:"Unspecified"}),", the input is returned unchanged."]}),"\n",(0,t.jsxs)(i.li,{children:["If the configured ",(0,t.jsx)(i.code,{children:"Kind"})," matches the input's ",(0,t.jsx)(i.code,{children:"Kind"}),", no conversion occurs."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.code,{children:"Local"})," + UTC input: converts to local time via ",(0,t.jsx)(i.code,{children:"ToLocalTime()"}),"."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.code,{children:"Utc"})," + Local input: converts to UTC via ",(0,t.jsx)(i.code,{children:"ToUniversalTime()"}),"."]}),"\n",(0,t.jsxs)(i.li,{children:[(0,t.jsx)(i.code,{children:"Unspecified"})," input: re-specifies the ",(0,t.jsx)(i.code,{children:"Kind"})," without converting the value."]}),"\n"]}),"\n",(0,t.jsx)(i.h3,{id:"in-unit-tests",children:"In unit tests"}),"\n",(0,t.jsxs)(i.p,{children:["Substitute ",(0,t.jsx)(i.code,{children:"ISystemTime"})," with a test double that returns a known timestamp:"]}),"\n",(0,t.jsx)(i.pre,{children:(0,t.jsx)(i.code,{className:"language-csharp",children:'public class FakeSystemTime : ISystemTime\r\n{\r\n private readonly DateTime _fixedTime;\r\n\r\n public FakeSystemTime(DateTime fixedTime)\r\n {\r\n _fixedTime = fixedTime;\r\n }\r\n\r\n public DateTime Now => _fixedTime;\r\n public DateTimeKind Kind => _fixedTime.Kind;\r\n public bool SupportsMultipleTimezone => _fixedTime.Kind == DateTimeKind.Utc;\r\n public DateTime Normalize(DateTime dateTime) => dateTime;\r\n}\r\n\r\n// In a test\r\nvar fixedTime = new DateTime(2026, 1, 15, 9, 0, 0, DateTimeKind.Utc);\r\nvar fakeTime = new FakeSystemTime(fixedTime);\r\nvar service = new AuditService(fakeTime, repositoryMock.Object);\r\n\r\nawait service.RecordEvent("order placed");\r\n\r\nrepositoryMock.Verify(r => r.AddAsync(It.Is(e => e.OccurredAt == fixedTime)));\n'})}),"\n",(0,t.jsx)(i.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,t.jsx)(i.h3,{id:"isystemtime",children:(0,t.jsx)(i.code,{children:"ISystemTime"})}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Member"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsxs)(i.tbody,{children:[(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DateTime Now { get; }"})}),(0,t.jsxs)(i.td,{children:["Returns the current date and time according to the configured ",(0,t.jsx)(i.code,{children:"Kind"}),"."]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DateTimeKind Kind { get; }"})}),(0,t.jsxs)(i.td,{children:["The ",(0,t.jsx)(i.code,{children:"DateTimeKind"})," this instance was configured with."]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"bool SupportsMultipleTimezone { get; }"})}),(0,t.jsxs)(i.td,{children:[(0,t.jsx)(i.code,{children:"true"})," when ",(0,t.jsx)(i.code,{children:"Kind"})," is ",(0,t.jsx)(i.code,{children:"Utc"}),", indicating the time is safe to store across time zones."]})]}),(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DateTime Normalize(DateTime dateTime)"})}),(0,t.jsxs)(i.td,{children:["Converts the provided ",(0,t.jsx)(i.code,{children:"DateTime"})," to match the configured ",(0,t.jsx)(i.code,{children:"Kind"}),"."]})]})]})]}),"\n",(0,t.jsxs)(i.h3,{id:"systemtime-default-implementation",children:[(0,t.jsx)(i.code,{children:"SystemTime"})," (default implementation)"]}),"\n",(0,t.jsxs)(i.p,{children:["Registered as ",(0,t.jsx)(i.code,{children:"ISystemTime"})," by ",(0,t.jsx)(i.code,{children:"WithDateTimeSystem(...)"}),". Transient lifetime. ",(0,t.jsx)(i.code,{children:"Now"})," returns ",(0,t.jsx)(i.code,{children:"DateTime.UtcNow"})," when ",(0,t.jsx)(i.code,{children:"Kind"})," is ",(0,t.jsx)(i.code,{children:"Utc"}),", otherwise ",(0,t.jsx)(i.code,{children:"DateTime.Now"}),"."]}),"\n",(0,t.jsx)(i.h3,{id:"systemtimeoptions",children:(0,t.jsx)(i.code,{children:"SystemTimeOptions"})}),"\n",(0,t.jsxs)(i.table,{children:[(0,t.jsx)(i.thead,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.th,{children:"Property"}),(0,t.jsx)(i.th,{children:"Type"}),(0,t.jsx)(i.th,{children:"Default"}),(0,t.jsx)(i.th,{children:"Description"})]})}),(0,t.jsx)(i.tbody,{children:(0,t.jsxs)(i.tr,{children:[(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"Kind"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DateTimeKind"})}),(0,t.jsx)(i.td,{children:(0,t.jsx)(i.code,{children:"DateTimeKind.Unspecified"})}),(0,t.jsx)(i.td,{children:"Controls whether the system clock operates in UTC, Local, or Unspecified mode."})]})})]})]})}function h(e={}){const{wrapper:i}={...(0,s.R)(),...e.components};return i?(0,t.jsx)(i,{...e,children:(0,t.jsx)(m,{...e})}):m(e)}},75783(e,i,n){n.d(i,{A:()=>a});var t=n(30758);const s="container_xjrG",r="label_Y4p8",c="commandRow_FY5I",d="command_m7Qs",o="copyButton_u1GK";var l=n(86070);function a({packageName:e,version:i}){const[n,a]=(0,t.useState)(!1),m=i?`dotnet add package ${e} --version ${i}`:`dotnet add package ${e}`;return(0,l.jsxs)("div",{className:s,children:[(0,l.jsx)("div",{className:r,children:"NuGet Package"}),(0,l.jsxs)("div",{className:c,children:[(0,l.jsx)("code",{className:d,children:m}),(0,l.jsx)("button",{className:o,onClick:()=>{navigator.clipboard.writeText(m),a(!0),setTimeout(()=>a(!1),2e3)},title:"Copy to clipboard",children:n?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,i,n){n.d(i,{R:()=>c,x:()=>d});var t=n(30758);const s={},r=t.createContext(s);function c(e){const i=t.useContext(r);return t.useMemo(function(){return"function"==typeof e?e(i):{...i,...e}},[i,e])}function d(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:c(e.components),t.createElement(r.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/f0840d59.02df58bd.js b/website/build/assets/js/f0840d59.02df58bd.js new file mode 100644 index 00000000..53e864cc --- /dev/null +++ b/website/build/assets/js/f0840d59.02df58bd.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[9315],{44393(e,n,i){i.r(n),i.d(n,{assets:()=>c,contentTitle:()=>a,default:()=>h,frontMatter:()=>t,metadata:()=>l,toc:()=>o});var s=i(86070),d=i(81753),r=i(75783);const t={title:"SendGrid",sidebar_position:2,description:"RCommon.SendGrid implements IEmailService via the SendGrid API, converting MailMessage objects to SendGrid calls with attachment and multi-recipient support."},a="SendGrid",l={id:"email/sendgrid",title:"SendGrid",description:"RCommon.SendGrid implements IEmailService via the SendGrid API, converting MailMessage objects to SendGrid calls with attachment and multi-recipient support.",source:"@site/docs/email/sendgrid.mdx",sourceDirName:"email",slug:"/email/sendgrid",permalink:"/docs/next/email/sendgrid",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/email/sendgrid.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"SendGrid",sidebar_position:2,description:"RCommon.SendGrid implements IEmailService via the SendGrid API, converting MailMessage objects to SendGrid calls with attachment and multi-recipient support."},sidebar:"docsSidebar",previous:{title:"Overview",permalink:"/docs/next/email/overview"},next:{title:"Multi-Tenancy",permalink:"/docs/next/category/multi-tenancy"}},c={},o=[{value:"Overview",id:"overview",level:2},{value:"How it works",id:"how-it-works",level:3},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Binding from configuration",id:"binding-from-configuration",level:3},{value:"Usage",id:"usage",level:2},{value:"Sending a plain-text email",id:"sending-a-plain-text-email",level:3},{value:"Sending an HTML email",id:"sending-an-html-email",level:3},{value:"Sending to multiple recipients",id:"sending-to-multiple-recipients",level:3},{value:"Sending with an attachment",id:"sending-with-an-attachment",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"SendGridEmailService",id:"sendgridemailservice",level:3},{value:"SendGridEmailSettings",id:"sendgridemailsettings",level:3},{value:"IRCommonBuilder extension",id:"ircommonbuilder-extension",level:3}];function m(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,d.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"sendgrid",children:"SendGrid"}),"\n",(0,s.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"RCommon.SendGrid"})," provides a ",(0,s.jsx)(n.code,{children:"SendGridEmailService"})," that implements ",(0,s.jsx)(n.code,{children:"IEmailService"})," using the official SendGrid .NET SDK. Your application code continues to work with standard ",(0,s.jsx)(n.code,{children:"System.Net.Mail.MailMessage"})," objects; the implementation converts them to SendGrid API calls transparently."]}),"\n",(0,s.jsx)(n.p,{children:"Switching from SMTP to SendGrid is a startup configuration change \u2014 no application code needs to be modified."}),"\n",(0,s.jsx)(n.h3,{id:"how-it-works",children:"How it works"}),"\n",(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"SendGridEmailService"})," accepts a ",(0,s.jsx)(n.code,{children:"MailMessage"})," and converts it to a ",(0,s.jsx)(n.code,{children:"SendGridMessage"})," using ",(0,s.jsx)(n.code,{children:"MailHelper.CreateSingleEmailToMultipleRecipients"}),". The body is sent as HTML when ",(0,s.jsx)(n.code,{children:"MailMessage.IsBodyHtml"})," is ",(0,s.jsx)(n.code,{children:"true"}),", otherwise as plain text. Attachments are streamed directly into the SendGrid message before the API call is made."]}),"\n",(0,s.jsxs)(n.p,{children:["The synchronous ",(0,s.jsx)(n.code,{children:"SendEmail"})," method wraps the async implementation using ",(0,s.jsx)(n.code,{children:"AsyncHelper.RunSync"}),"."]}),"\n",(0,s.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,s.jsx)(r.A,{packageName:"RCommon.SendGrid"}),"\n",(0,s.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,s.jsxs)(n.p,{children:["Call ",(0,s.jsx)(n.code,{children:"WithSendGridEmailServices"})," inside your ",(0,s.jsx)(n.code,{children:"AddRCommon()"})," block and provide a delegate that configures ",(0,s.jsx)(n.code,{children:"SendGridEmailSettings"}),"."]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'using RCommon;\n\nbuilder.Services.AddRCommon()\n .WithSendGridEmailServices(sg =>\n {\n sg.SendGridApiKey = builder.Configuration["SendGrid:ApiKey"];\n sg.FromEmailDefault = "noreply@example.com";\n sg.FromNameDefault = "My Application";\n });\n'})}),"\n",(0,s.jsx)(n.p,{children:"Store the API key in a secrets manager or environment variable, never in source-controlled configuration files:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-json",children:'{\n "SendGrid": {\n "ApiKey": "",\n "FromEmailDefault": "noreply@example.com",\n "FromNameDefault": "My Application"\n }\n}\n'})}),"\n",(0,s.jsxs)(n.p,{children:["Set ",(0,s.jsx)(n.code,{children:"SendGrid:ApiKey"})," via:"]}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.code,{children:'dotnet user-secrets set "SendGrid:ApiKey" "SG.your_key_here"'})," during development"]}),"\n",(0,s.jsxs)(n.li,{children:["An environment variable ",(0,s.jsx)(n.code,{children:"SendGrid__ApiKey"})," in production containers"]}),"\n",(0,s.jsx)(n.li,{children:"Azure Key Vault, AWS Secrets Manager, or your preferred secrets service"}),"\n"]}),"\n",(0,s.jsx)(n.h3,{id:"binding-from-configuration",children:"Binding from configuration"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'builder.Services.AddRCommon()\n .WithSendGridEmailServices(sg =>\n builder.Configuration.GetSection("SendGrid").Bind(sg));\n'})}),"\n",(0,s.jsx)(n.admonition,{title:"Modular composition",type:"tip",children:(0,s.jsxs)(n.p,{children:[(0,s.jsx)(n.code,{children:"WithSendGridEmailServices"})," is a singleton-style verb: only one ",(0,s.jsx)(n.code,{children:"IEmailService"})," implementation makes sense per process. Calling ",(0,s.jsx)(n.code,{children:"WithSendGridEmailServices"})," from two modules is an idempotent no-op \u2014 the second registration is skipped, and ",(0,s.jsx)(n.code,{children:"SendGridEmailSettings"})," follows last-write-wins for any options the second module's delegate assigns. Mixing ",(0,s.jsx)(n.code,{children:"WithSendGridEmailServices"})," and ",(0,s.jsx)(n.code,{children:"WithSmtpEmailServices"})," in the same process throws ",(0,s.jsx)(n.code,{children:"RCommonBuilderException"})," naming both impl types; pick exactly one transport. See ",(0,s.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," and the ",(0,s.jsx)(n.a,{href:"/docs/next/email/overview#modular-composition",children:"Email overview"})," for the full conflict matrix."]})}),"\n",(0,s.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,s.jsxs)(n.p,{children:["Inject ",(0,s.jsx)(n.code,{children:"IEmailService"})," exactly as you would with SMTP. No SendGrid-specific types appear in application code."]}),"\n",(0,s.jsx)(n.h3,{id:"sending-a-plain-text-email",children:"Sending a plain-text email"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'using System.Net.Mail;\n\npublic class NotificationService\n{\n private readonly IEmailService _emailService;\n\n public NotificationService(IEmailService emailService)\n {\n _emailService = emailService;\n }\n\n public async Task SendPasswordResetAsync(string email,\n string resetLink, CancellationToken ct = default)\n {\n using var message = new MailMessage(\n from: "noreply@example.com",\n to: email,\n subject: "Reset your password",\n body: $"Follow this link to reset your password: {resetLink}");\n\n await _emailService.SendEmailAsync(message, ct);\n }\n}\n'})}),"\n",(0,s.jsx)(n.h3,{id:"sending-an-html-email",children:"Sending an HTML email"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'using var message = new MailMessage\n{\n From = new MailAddress("noreply@example.com", "My App"),\n Subject = "Your invoice is ready",\n Body = "

Your invoice for this month is now available.

",\n IsBodyHtml = true\n};\nmessage.To.Add(new MailAddress("customer@example.com", "Jane Doe"));\n\nawait _emailService.SendEmailAsync(message, ct);\n'})}),"\n",(0,s.jsx)(n.h3,{id:"sending-to-multiple-recipients",children:"Sending to multiple recipients"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'using var message = new MailMessage\n{\n From = new MailAddress("alerts@example.com"),\n Subject = "Scheduled maintenance",\n Body = "The system will be offline on Saturday from 02:00 to 04:00 UTC.",\n IsBodyHtml = false\n};\nmessage.To.Add(new MailAddress("alice@example.com"));\nmessage.To.Add(new MailAddress("bob@example.com"));\n\nawait _emailService.SendEmailAsync(message, ct);\n'})}),"\n",(0,s.jsx)(n.h3,{id:"sending-with-an-attachment",children:"Sending with an attachment"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-csharp",children:'using var message = new MailMessage(\n "reports@example.com", "finance@example.com")\n{\n Subject = "Q1 Report",\n Body = "

Please find the Q1 report attached.

",\n IsBodyHtml = true\n};\n\nawait using var pdf = File.OpenRead("/tmp/q1-report.pdf");\nmessage.Attachments.Add(new Attachment(pdf, "q1-report.pdf", "application/pdf"));\n\nawait _emailService.SendEmailAsync(message, ct);\n'})}),"\n",(0,s.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,s.jsx)(n.h3,{id:"sendgridemailservice",children:(0,s.jsx)(n.code,{children:"SendGridEmailService"})}),"\n",(0,s.jsxs)(n.p,{children:["Registered as ",(0,s.jsx)(n.code,{children:"IEmailService"}),". Accepts ",(0,s.jsx)(n.code,{children:"IOptions"})," via constructor injection and constructs a ",(0,s.jsx)(n.code,{children:"SendGridClient"})," internally."]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Member"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"SendEmail(MailMessage)"})}),(0,s.jsxs)(n.td,{children:["Synchronous wrapper around ",(0,s.jsx)(n.code,{children:"SendEmailAsync"}),"."]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"SendEmailAsync(MailMessage, CancellationToken)"})}),(0,s.jsxs)(n.td,{children:["Converts the message and sends it via the SendGrid API. No-op when ",(0,s.jsx)(n.code,{children:"message.To"})," is empty."]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"EmailSent"})}),(0,s.jsx)(n.td,{children:"Raised after the SendGrid API call completes successfully."})]})]})]}),"\n",(0,s.jsx)(n.h3,{id:"sendgridemailsettings",children:(0,s.jsx)(n.code,{children:"SendGridEmailSettings"})}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Property"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsxs)(n.tbody,{children:[(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"SendGridApiKey"})}),(0,s.jsx)(n.td,{children:"The SendGrid API key used to authenticate all API calls."})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"FromEmailDefault"})}),(0,s.jsxs)(n.td,{children:["Default sender email address when no ",(0,s.jsx)(n.code,{children:"From"})," address is set on the ",(0,s.jsx)(n.code,{children:"MailMessage"}),"."]})]}),(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"FromNameDefault"})}),(0,s.jsx)(n.td,{children:"Default sender display name."})]})]})]}),"\n",(0,s.jsxs)(n.h3,{id:"ircommonbuilder-extension",children:[(0,s.jsx)(n.code,{children:"IRCommonBuilder"})," extension"]}),"\n",(0,s.jsxs)(n.table,{children:[(0,s.jsx)(n.thead,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.th,{children:"Method"}),(0,s.jsx)(n.th,{children:"Description"})]})}),(0,s.jsx)(n.tbody,{children:(0,s.jsxs)(n.tr,{children:[(0,s.jsx)(n.td,{children:(0,s.jsx)(n.code,{children:"WithSendGridEmailServices(Action)"})}),(0,s.jsxs)(n.td,{children:["Registers ",(0,s.jsx)(n.code,{children:"SendGridEmailService"})," as ",(0,s.jsx)(n.code,{children:"IEmailService"})," and configures SendGrid settings."]})]})})]})]})}function h(e={}){const{wrapper:n}={...(0,d.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(m,{...e})}):m(e)}},75783(e,n,i){i.d(n,{A:()=>o});var s=i(30758);const d="container_xjrG",r="label_Y4p8",t="commandRow_FY5I",a="command_m7Qs",l="copyButton_u1GK";var c=i(86070);function o({packageName:e,version:n}){const[i,o]=(0,s.useState)(!1),m=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,c.jsxs)("div",{className:d,children:[(0,c.jsx)("div",{className:r,children:"NuGet Package"}),(0,c.jsxs)("div",{className:t,children:[(0,c.jsx)("code",{className:a,children:m}),(0,c.jsx)("button",{className:l,onClick:()=>{navigator.clipboard.writeText(m),o(!0),setTimeout(()=>o(!1),2e3)},title:"Copy to clipboard",children:i?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,i){i.d(n,{R:()=>t,x:()=>a});var s=i(30758);const d={},r=s.createContext(d);function t(e){const n=s.useContext(r);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(d):e.components||d:t(e.components),s.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/f0840d59.7296e5b3.js b/website/build/assets/js/f0840d59.7296e5b3.js deleted file mode 100644 index 31520648..00000000 --- a/website/build/assets/js/f0840d59.7296e5b3.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[9315],{44393(e,n,i){i.r(n),i.d(n,{assets:()=>c,contentTitle:()=>a,default:()=>h,frontMatter:()=>t,metadata:()=>l,toc:()=>o});var r=i(86070),s=i(81753),d=i(75783);const t={title:"SendGrid",sidebar_position:2,description:"RCommon.SendGrid implements IEmailService via the SendGrid API, converting MailMessage objects to SendGrid calls with attachment and multi-recipient support."},a="SendGrid",l={id:"email/sendgrid",title:"SendGrid",description:"RCommon.SendGrid implements IEmailService via the SendGrid API, converting MailMessage objects to SendGrid calls with attachment and multi-recipient support.",source:"@site/docs/email/sendgrid.mdx",sourceDirName:"email",slug:"/email/sendgrid",permalink:"/docs/next/email/sendgrid",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/email/sendgrid.mdx",tags:[],version:"current",sidebarPosition:2,frontMatter:{title:"SendGrid",sidebar_position:2,description:"RCommon.SendGrid implements IEmailService via the SendGrid API, converting MailMessage objects to SendGrid calls with attachment and multi-recipient support."},sidebar:"docsSidebar",previous:{title:"Overview",permalink:"/docs/next/email/overview"},next:{title:"Multi-Tenancy",permalink:"/docs/next/category/multi-tenancy"}},c={},o=[{value:"Overview",id:"overview",level:2},{value:"How it works",id:"how-it-works",level:3},{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Binding from configuration",id:"binding-from-configuration",level:3},{value:"Usage",id:"usage",level:2},{value:"Sending a plain-text email",id:"sending-a-plain-text-email",level:3},{value:"Sending an HTML email",id:"sending-an-html-email",level:3},{value:"Sending to multiple recipients",id:"sending-to-multiple-recipients",level:3},{value:"Sending with an attachment",id:"sending-with-an-attachment",level:3},{value:"API Summary",id:"api-summary",level:2},{value:"SendGridEmailService",id:"sendgridemailservice",level:3},{value:"SendGridEmailSettings",id:"sendgridemailsettings",level:3},{value:"IRCommonBuilder extension",id:"ircommonbuilder-extension",level:3}];function m(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",ul:"ul",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"sendgrid",children:"SendGrid"}),"\n",(0,r.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"RCommon.SendGrid"})," provides a ",(0,r.jsx)(n.code,{children:"SendGridEmailService"})," that implements ",(0,r.jsx)(n.code,{children:"IEmailService"})," using the official SendGrid .NET SDK. Your application code continues to work with standard ",(0,r.jsx)(n.code,{children:"System.Net.Mail.MailMessage"})," objects; the implementation converts them to SendGrid API calls transparently."]}),"\n",(0,r.jsx)(n.p,{children:"Switching from SMTP to SendGrid is a startup configuration change \u2014 no application code needs to be modified."}),"\n",(0,r.jsx)(n.h3,{id:"how-it-works",children:"How it works"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"SendGridEmailService"})," accepts a ",(0,r.jsx)(n.code,{children:"MailMessage"})," and converts it to a ",(0,r.jsx)(n.code,{children:"SendGridMessage"})," using ",(0,r.jsx)(n.code,{children:"MailHelper.CreateSingleEmailToMultipleRecipients"}),". The body is sent as HTML when ",(0,r.jsx)(n.code,{children:"MailMessage.IsBodyHtml"})," is ",(0,r.jsx)(n.code,{children:"true"}),", otherwise as plain text. Attachments are streamed directly into the SendGrid message before the API call is made."]}),"\n",(0,r.jsxs)(n.p,{children:["The synchronous ",(0,r.jsx)(n.code,{children:"SendEmail"})," method wraps the async implementation using ",(0,r.jsx)(n.code,{children:"AsyncHelper.RunSync"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(d.A,{packageName:"RCommon.SendGrid"}),"\n",(0,r.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsxs)(n.p,{children:["Call ",(0,r.jsx)(n.code,{children:"WithSendGridEmailServices"})," inside your ",(0,r.jsx)(n.code,{children:"AddRCommon()"})," block and provide a delegate that configures ",(0,r.jsx)(n.code,{children:"SendGridEmailSettings"}),"."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using RCommon;\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithSendGridEmailServices(sg =>\r\n {\r\n sg.SendGridApiKey = builder.Configuration["SendGrid:ApiKey"];\r\n sg.FromEmailDefault = "noreply@example.com";\r\n sg.FromNameDefault = "My Application";\r\n });\n'})}),"\n",(0,r.jsx)(n.p,{children:"Store the API key in a secrets manager or environment variable, never in source-controlled configuration files:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-json",children:'{\r\n "SendGrid": {\r\n "ApiKey": "",\r\n "FromEmailDefault": "noreply@example.com",\r\n "FromNameDefault": "My Application"\r\n }\r\n}\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Set ",(0,r.jsx)(n.code,{children:"SendGrid:ApiKey"})," via:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:'dotnet user-secrets set "SendGrid:ApiKey" "SG.your_key_here"'})," during development"]}),"\n",(0,r.jsxs)(n.li,{children:["An environment variable ",(0,r.jsx)(n.code,{children:"SendGrid__ApiKey"})," in production containers"]}),"\n",(0,r.jsx)(n.li,{children:"Azure Key Vault, AWS Secrets Manager, or your preferred secrets service"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"binding-from-configuration",children:"Binding from configuration"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'builder.Services.AddRCommon()\r\n .WithSendGridEmailServices(sg =>\r\n builder.Configuration.GetSection("SendGrid").Bind(sg));\n'})}),"\n",(0,r.jsx)(n.h2,{id:"usage",children:"Usage"}),"\n",(0,r.jsxs)(n.p,{children:["Inject ",(0,r.jsx)(n.code,{children:"IEmailService"})," exactly as you would with SMTP. No SendGrid-specific types appear in application code."]}),"\n",(0,r.jsx)(n.h3,{id:"sending-a-plain-text-email",children:"Sending a plain-text email"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using System.Net.Mail;\r\n\r\npublic class NotificationService\r\n{\r\n private readonly IEmailService _emailService;\r\n\r\n public NotificationService(IEmailService emailService)\r\n {\r\n _emailService = emailService;\r\n }\r\n\r\n public async Task SendPasswordResetAsync(string email,\r\n string resetLink, CancellationToken ct = default)\r\n {\r\n using var message = new MailMessage(\r\n from: "noreply@example.com",\r\n to: email,\r\n subject: "Reset your password",\r\n body: $"Follow this link to reset your password: {resetLink}");\r\n\r\n await _emailService.SendEmailAsync(message, ct);\r\n }\r\n}\n'})}),"\n",(0,r.jsx)(n.h3,{id:"sending-an-html-email",children:"Sending an HTML email"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using var message = new MailMessage\r\n{\r\n From = new MailAddress("noreply@example.com", "My App"),\r\n Subject = "Your invoice is ready",\r\n Body = "

Your invoice for this month is now available.

",\r\n IsBodyHtml = true\r\n};\r\nmessage.To.Add(new MailAddress("customer@example.com", "Jane Doe"));\r\n\r\nawait _emailService.SendEmailAsync(message, ct);\n'})}),"\n",(0,r.jsx)(n.h3,{id:"sending-to-multiple-recipients",children:"Sending to multiple recipients"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using var message = new MailMessage\r\n{\r\n From = new MailAddress("alerts@example.com"),\r\n Subject = "Scheduled maintenance",\r\n Body = "The system will be offline on Saturday from 02:00 to 04:00 UTC.",\r\n IsBodyHtml = false\r\n};\r\nmessage.To.Add(new MailAddress("alice@example.com"));\r\nmessage.To.Add(new MailAddress("bob@example.com"));\r\n\r\nawait _emailService.SendEmailAsync(message, ct);\n'})}),"\n",(0,r.jsx)(n.h3,{id:"sending-with-an-attachment",children:"Sending with an attachment"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-csharp",children:'using var message = new MailMessage(\r\n "reports@example.com", "finance@example.com")\r\n{\r\n Subject = "Q1 Report",\r\n Body = "

Please find the Q1 report attached.

",\r\n IsBodyHtml = true\r\n};\r\n\r\nawait using var pdf = File.OpenRead("/tmp/q1-report.pdf");\r\nmessage.Attachments.Add(new Attachment(pdf, "q1-report.pdf", "application/pdf"));\r\n\r\nawait _emailService.SendEmailAsync(message, ct);\n'})}),"\n",(0,r.jsx)(n.h2,{id:"api-summary",children:"API Summary"}),"\n",(0,r.jsx)(n.h3,{id:"sendgridemailservice",children:(0,r.jsx)(n.code,{children:"SendGridEmailService"})}),"\n",(0,r.jsxs)(n.p,{children:["Registered as ",(0,r.jsx)(n.code,{children:"IEmailService"}),". Accepts ",(0,r.jsx)(n.code,{children:"IOptions"})," via constructor injection and constructs a ",(0,r.jsx)(n.code,{children:"SendGridClient"})," internally."]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Member"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SendEmail(MailMessage)"})}),(0,r.jsxs)(n.td,{children:["Synchronous wrapper around ",(0,r.jsx)(n.code,{children:"SendEmailAsync"}),"."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SendEmailAsync(MailMessage, CancellationToken)"})}),(0,r.jsxs)(n.td,{children:["Converts the message and sends it via the SendGrid API. No-op when ",(0,r.jsx)(n.code,{children:"message.To"})," is empty."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"EmailSent"})}),(0,r.jsx)(n.td,{children:"Raised after the SendGrid API call completes successfully."})]})]})]}),"\n",(0,r.jsx)(n.h3,{id:"sendgridemailsettings",children:(0,r.jsx)(n.code,{children:"SendGridEmailSettings"})}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Property"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsxs)(n.tbody,{children:[(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"SendGridApiKey"})}),(0,r.jsx)(n.td,{children:"The SendGrid API key used to authenticate all API calls."})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"FromEmailDefault"})}),(0,r.jsxs)(n.td,{children:["Default sender email address when no ",(0,r.jsx)(n.code,{children:"From"})," address is set on the ",(0,r.jsx)(n.code,{children:"MailMessage"}),"."]})]}),(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"FromNameDefault"})}),(0,r.jsx)(n.td,{children:"Default sender display name."})]})]})]}),"\n",(0,r.jsxs)(n.h3,{id:"ircommonbuilder-extension",children:[(0,r.jsx)(n.code,{children:"IRCommonBuilder"})," extension"]}),"\n",(0,r.jsxs)(n.table,{children:[(0,r.jsx)(n.thead,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.th,{children:"Method"}),(0,r.jsx)(n.th,{children:"Description"})]})}),(0,r.jsx)(n.tbody,{children:(0,r.jsxs)(n.tr,{children:[(0,r.jsx)(n.td,{children:(0,r.jsx)(n.code,{children:"WithSendGridEmailServices(Action)"})}),(0,r.jsxs)(n.td,{children:["Registers ",(0,r.jsx)(n.code,{children:"SendGridEmailService"})," as ",(0,r.jsx)(n.code,{children:"IEmailService"})," and configures SendGrid settings."]})]})})]})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(m,{...e})}):m(e)}},75783(e,n,i){i.d(n,{A:()=>o});var r=i(30758);const s="container_xjrG",d="label_Y4p8",t="commandRow_FY5I",a="command_m7Qs",l="copyButton_u1GK";var c=i(86070);function o({packageName:e,version:n}){const[i,o]=(0,r.useState)(!1),m=n?`dotnet add package ${e} --version ${n}`:`dotnet add package ${e}`;return(0,c.jsxs)("div",{className:s,children:[(0,c.jsx)("div",{className:d,children:"NuGet Package"}),(0,c.jsxs)("div",{className:t,children:[(0,c.jsx)("code",{className:a,children:m}),(0,c.jsx)("button",{className:l,onClick:()=>{navigator.clipboard.writeText(m),o(!0),setTimeout(()=>o(!1),2e3)},title:"Copy to clipboard",children:i?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,n,i){i.d(n,{R:()=>t,x:()=>a});var r=i(30758);const s={},d=r.createContext(s);function t(e){const n=r.useContext(d);return r.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:t(e.components),r.createElement(d.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/f66b7bee.5a87cf53.js b/website/build/assets/js/f66b7bee.5a87cf53.js deleted file mode 100644 index 2e1647df..00000000 --- a/website/build/assets/js/f66b7bee.5a87cf53.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[9714],{95760(e,r,n){n.r(r),n.d(r,{assets:()=>d,contentTitle:()=>o,default:()=>h,frontMatter:()=>s,metadata:()=>c,toc:()=>a});var i=n(86070),t=n(81753);const s={title:"Dependency Injection",sidebar_position:5,description:"How RCommon integrates with Microsoft.Extensions.DependencyInjection \u2014 service lifetimes, injecting repositories, IEventBus, ISystemTime, and named data stores."},o="Dependency Injection",c={id:"getting-started/dependency-injection",title:"Dependency Injection",description:"How RCommon integrates with Microsoft.Extensions.DependencyInjection \u2014 service lifetimes, injecting repositories, IEventBus, ISystemTime, and named data stores.",source:"@site/docs/getting-started/dependency-injection.mdx",sourceDirName:"getting-started",slug:"/getting-started/dependency-injection",permalink:"/docs/next/getting-started/dependency-injection",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/getting-started/dependency-injection.mdx",tags:[],version:"current",sidebarPosition:5,frontMatter:{title:"Dependency Injection",sidebar_position:5,description:"How RCommon integrates with Microsoft.Extensions.DependencyInjection \u2014 service lifetimes, injecting repositories, IEventBus, ISystemTime, and named data stores."},sidebar:"docsSidebar",previous:{title:"Configuration & Bootstrapping",permalink:"/docs/next/getting-started/configuration"},next:{title:"Core Concepts",permalink:"/docs/next/category/core-concepts"}},d={},a=[{value:"How RCommon registers services",id:"how-rcommon-registers-services",level:2},{value:"Service lifetimes used by RCommon",id:"service-lifetimes-used-by-rcommon",level:2},{value:"Injecting RCommon services",id:"injecting-rcommon-services",level:2},{value:"Repositories",id:"repositories",level:3},{value:"IEventBus",id:"ieventbus",level:3},{value:"ISystemTime",id:"isystemtime",level:3},{value:"IGuidGenerator",id:"iguidgenerator",level:3},{value:"ICommonFactory<T>",id:"icommonfactoryt",level:3},{value:"Service registration patterns",id:"service-registration-patterns",level:2},{value:"Registering your own services alongside RCommon",id:"registering-your-own-services-alongside-rcommon",level:3},{value:"Named data stores",id:"named-data-stores",level:3},{value:"Verifying registrations",id:"verifying-registrations",level:2}];function l(e){const r={code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,t.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(r.h1,{id:"dependency-injection",children:"Dependency Injection"}),"\n",(0,i.jsxs)(r.p,{children:["RCommon is built entirely on ",(0,i.jsx)(r.code,{children:"Microsoft.Extensions.DependencyInjection"}),". It does not introduce its own container or wrap the standard one. Every service RCommon registers follows the same lifetime conventions you already know from ASP.NET Core."]}),"\n",(0,i.jsx)(r.h2,{id:"how-rcommon-registers-services",children:"How RCommon registers services"}),"\n",(0,i.jsxs)(r.p,{children:["When you call ",(0,i.jsx)(r.code,{children:"AddRCommon()"}),", the ",(0,i.jsx)(r.code,{children:"RCommonBuilder"})," receives the application's ",(0,i.jsx)(r.code,{children:"IServiceCollection"})," and calls ",(0,i.jsx)(r.code,{children:"AddSingleton"}),", ",(0,i.jsx)(r.code,{children:"AddScoped"}),", and ",(0,i.jsx)(r.code,{children:"AddTransient"})," on it directly. By the time ",(0,i.jsx)(r.code,{children:"builder.Build()"})," is called, all RCommon services are in the same container as your application services \u2014 no separate container, no child scope complications."]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"// All of this is standard Microsoft DI\r\nvar builder = WebApplication.CreateBuilder(args);\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithPersistence(ef => { ... })\r\n .WithMediator(m => { ... });\r\n\r\nvar app = builder.Build();\n"})}),"\n",(0,i.jsx)(r.h2,{id:"service-lifetimes-used-by-rcommon",children:"Service lifetimes used by RCommon"}),"\n",(0,i.jsx)(r.p,{children:"Understanding which lifetime each service uses helps you reason about when instances are created and shared."}),"\n",(0,i.jsxs)(r.table,{children:[(0,i.jsx)(r.thead,{children:(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.th,{children:"Service"}),(0,i.jsx)(r.th,{children:"Lifetime"}),(0,i.jsx)(r.th,{children:"Reason"})]})}),(0,i.jsxs)(r.tbody,{children:[(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"EventSubscriptionManager"})}),(0,i.jsx)(r.td,{children:"Singleton"}),(0,i.jsx)(r.td,{children:"Subscription metadata is static; built once at startup"})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsxs)(r.td,{children:[(0,i.jsx)(r.code,{children:"IEventBus"})," (",(0,i.jsx)(r.code,{children:"InMemoryEventBus"}),")"]}),(0,i.jsx)(r.td,{children:"Singleton"}),(0,i.jsx)(r.td,{children:"Stateless dispatcher; safe to share across requests"})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsxs)(r.td,{children:[(0,i.jsx)(r.code,{children:"IEventRouter"})," (",(0,i.jsx)(r.code,{children:"InMemoryTransactionalEventRouter"}),")"]}),(0,i.jsx)(r.td,{children:"Scoped"}),(0,i.jsx)(r.td,{children:"Tied to the unit of work scope; one per HTTP request"})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsxs)(r.td,{children:[(0,i.jsx)(r.code,{children:"IEntityEventTracker"})," (",(0,i.jsx)(r.code,{children:"InMemoryEntityEventTracker"}),")"]}),(0,i.jsx)(r.td,{children:"Scoped"}),(0,i.jsx)(r.td,{children:"Accumulates domain events during a single unit of work"})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsxs)(r.td,{children:[(0,i.jsx)(r.code,{children:"ISubscriber"})," handler"]}),(0,i.jsx)(r.td,{children:"Scoped"}),(0,i.jsx)(r.td,{children:"Event handlers may depend on scoped services like repositories"})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsxs)(r.td,{children:[(0,i.jsx)(r.code,{children:"ILinqRepository"})," / ",(0,i.jsx)(r.code,{children:"IReadOnlyRepository"})," / ",(0,i.jsx)(r.code,{children:"IWriteOnlyRepository"})]}),(0,i.jsx)(r.td,{children:"Transient"}),(0,i.jsx)(r.td,{children:"Stateless wrappers around the scoped DbContext"})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"IAggregateRepository"})}),(0,i.jsx)(r.td,{children:"Transient"}),(0,i.jsx)(r.td,{children:"Same as above"})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"IUnitOfWork"})}),(0,i.jsx)(r.td,{children:"Transient"}),(0,i.jsx)(r.td,{children:"New scope created each time a unit of work is opened"})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"IUnitOfWorkFactory"})}),(0,i.jsx)(r.td,{children:"Transient"}),(0,i.jsx)(r.td,{children:"Lightweight factory; safe as transient"})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"IDataStoreFactory"})}),(0,i.jsx)(r.td,{children:"Transient"}),(0,i.jsx)(r.td,{children:"Resolves the correct DbContext by name; delegates to the scoped DbContext"})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsxs)(r.td,{children:[(0,i.jsx)(r.code,{children:"IGuidGenerator"})," (sequential)"]}),(0,i.jsx)(r.td,{children:"Transient"}),(0,i.jsx)(r.td,{children:"Stateless; new instance per call"})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsxs)(r.td,{children:[(0,i.jsx)(r.code,{children:"IGuidGenerator"})," (simple)"]}),(0,i.jsx)(r.td,{children:"Scoped"}),(0,i.jsxs)(r.td,{children:["Registered as scoped by ",(0,i.jsx)(r.code,{children:"WithSimpleGuidGenerator"})]})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ISystemTime"})}),(0,i.jsx)(r.td,{children:"Transient"}),(0,i.jsx)(r.td,{children:"Stateless clock wrapper"})]}),(0,i.jsxs)(r.tr,{children:[(0,i.jsx)(r.td,{children:(0,i.jsx)(r.code,{children:"ICommonFactory"})}),(0,i.jsx)(r.td,{children:"Scoped"}),(0,i.jsx)(r.td,{children:"Delegates to scoped service resolution"})]})]})]}),"\n",(0,i.jsx)(r.h2,{id:"injecting-rcommon-services",children:"Injecting RCommon services"}),"\n",(0,i.jsx)(r.h3,{id:"repositories",children:"Repositories"}),"\n",(0,i.jsxs)(r.p,{children:["After calling ",(0,i.jsx)(r.code,{children:"WithPersistence"}),", the open-generic repository interfaces are available for injection anywhere:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"public class OrderService\r\n{\r\n private readonly ILinqRepository _orders;\r\n\r\n public OrderService(ILinqRepository orders)\r\n {\r\n _orders = orders;\r\n }\r\n\r\n public async Task> GetPendingOrdersAsync()\r\n {\r\n return await _orders.FindAsync(o => o.Status == OrderStatus.Pending);\r\n }\r\n}\n"})}),"\n",(0,i.jsxs)(r.p,{children:["The DI container resolves ",(0,i.jsx)(r.code,{children:"ILinqRepository"})," to ",(0,i.jsx)(r.code,{children:"EFCoreRepository"})," which in turn receives the scoped ",(0,i.jsx)(r.code,{children:"AppDbContext"})," through constructor injection."]}),"\n",(0,i.jsx)(r.h3,{id:"ieventbus",children:"IEventBus"}),"\n",(0,i.jsxs)(r.p,{children:["Inject ",(0,i.jsx)(r.code,{children:"IEventBus"})," wherever you need to publish events in-process. It is a singleton, so it can be injected into any lifetime."]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"public class OrderApplicationService\r\n{\r\n private readonly ILinqRepository _orders;\r\n private readonly IEventBus _eventBus;\r\n\r\n public OrderApplicationService(\r\n ILinqRepository orders,\r\n IEventBus eventBus)\r\n {\r\n _orders = orders;\r\n _eventBus = eventBus;\r\n }\r\n\r\n public async Task PlaceOrderAsync(PlaceOrderRequest request)\r\n {\r\n var order = new Order(request.CustomerId, request.Items);\r\n await _orders.AddAsync(order);\r\n await _eventBus.PublishAsync(new OrderPlacedEvent(order.Id));\r\n }\r\n}\n"})}),"\n",(0,i.jsx)(r.h3,{id:"isystemtime",children:"ISystemTime"}),"\n",(0,i.jsxs)(r.p,{children:["Inject ",(0,i.jsx)(r.code,{children:"ISystemTime"})," instead of calling ",(0,i.jsx)(r.code,{children:"DateTime.UtcNow"})," directly so that tests can control the clock:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"public class AuditService\r\n{\r\n private readonly ISystemTime _clock;\r\n\r\n public AuditService(ISystemTime clock)\r\n {\r\n _clock = clock;\r\n }\r\n\r\n public DateTime GetCurrentTime() => _clock.Now;\r\n}\n"})}),"\n",(0,i.jsx)(r.h3,{id:"iguidgenerator",children:"IGuidGenerator"}),"\n",(0,i.jsxs)(r.p,{children:["Inject ",(0,i.jsx)(r.code,{children:"IGuidGenerator"})," to generate identifiers that are safe to use as primary keys:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"public class ProductFactory\r\n{\r\n private readonly IGuidGenerator _guidGenerator;\r\n\r\n public ProductFactory(IGuidGenerator guidGenerator)\r\n {\r\n _guidGenerator = guidGenerator;\r\n }\r\n\r\n public Product Create(string name, decimal price)\r\n {\r\n return new Product\r\n {\r\n Id = _guidGenerator.Create(),\r\n Name = name,\r\n Price = price\r\n };\r\n }\r\n}\n"})}),"\n",(0,i.jsx)(r.h3,{id:"icommonfactoryt",children:"ICommonFactory"}),"\n",(0,i.jsxs)(r.p,{children:[(0,i.jsx)(r.code,{children:"ICommonFactory"})," is a DI-aware factory that resolves instances of ",(0,i.jsx)(r.code,{children:"T"})," from the container on demand. Register it with ",(0,i.jsx)(r.code,{children:"WithCommonFactory"})," and then inject it where deferred or conditional resolution is needed:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"// Registration\r\nbuilder.Services.AddRCommon()\r\n .WithCommonFactory();\r\n\r\n// Usage\r\npublic class NotificationService\r\n{\r\n private readonly ICommonFactory _emailFactory;\r\n\r\n public NotificationService(ICommonFactory emailFactory)\r\n {\r\n _emailFactory = emailFactory;\r\n }\r\n\r\n public async Task SendAsync(string to, string subject, string body)\r\n {\r\n var sender = _emailFactory.Create();\r\n await sender.SendAsync(to, subject, body);\r\n }\r\n}\n"})}),"\n",(0,i.jsx)(r.h2,{id:"service-registration-patterns",children:"Service registration patterns"}),"\n",(0,i.jsx)(r.h3,{id:"registering-your-own-services-alongside-rcommon",children:"Registering your own services alongside RCommon"}),"\n",(0,i.jsxs)(r.p,{children:["RCommon does not prevent you from registering services before or after ",(0,i.jsx)(r.code,{children:"AddRCommon()"}),". Standard registrations work exactly as expected:"]}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"builder.Services.AddScoped();\r\n\r\nbuilder.Services.AddRCommon()\r\n .WithPersistence(ef => { ... });\r\n\r\nbuilder.Services.AddScoped();\n"})}),"\n",(0,i.jsx)(r.h3,{id:"named-data-stores",children:"Named data stores"}),"\n",(0,i.jsx)(r.p,{children:"When you register multiple DbContexts (for example a write model and a read model), each gets a name:"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:'.WithPersistence(ef =>\r\n{\r\n ef.AddDbContext("WriteDb", options =>\r\n options.UseSqlServer(writeConnectionString));\r\n\r\n ef.AddDbContext("ReadDb", options =>\r\n options.UseSqlServer(readConnectionString));\r\n\r\n ef.SetDefaultDataStore(ds =>\r\n ds.DefaultDataStoreName = "WriteDb");\r\n})\n'})}),"\n",(0,i.jsxs)(r.p,{children:["A repository can be directed to use a specific data store by setting ",(0,i.jsx)(r.code,{children:"DataStoreName"})," on the repository instance before use. The ",(0,i.jsx)(r.code,{children:"IDataStoreFactory"})," resolves the correct DbContext by name at runtime."]}),"\n",(0,i.jsx)(r.h2,{id:"verifying-registrations",children:"Verifying registrations"}),"\n",(0,i.jsx)(r.p,{children:"During development you can dump the full service registration list to the console:"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"Console.WriteLine(builder.Services.GenerateServiceDescriptorsString());\n"})}),"\n",(0,i.jsx)(r.p,{children:"To check for duplicate registrations \u2014 which can cause subtle bugs \u2014 use:"}),"\n",(0,i.jsx)(r.pre,{children:(0,i.jsx)(r.code,{className:"language-csharp",children:"Console.WriteLine(builder.Services.GeneratePossibleDuplicatesServiceDescriptorsString());\n"})}),"\n",(0,i.jsxs)(r.p,{children:["Both methods are extension methods on ",(0,i.jsx)(r.code,{children:"IServiceCollection"})," provided by ",(0,i.jsx)(r.code,{children:"RCommon.Core"}),"."]})]})}function h(e={}){const{wrapper:r}={...(0,t.R)(),...e.components};return r?(0,i.jsx)(r,{...e,children:(0,i.jsx)(l,{...e})}):l(e)}},81753(e,r,n){n.d(r,{R:()=>o,x:()=>c});var i=n(30758);const t={},s=i.createContext(t);function o(e){const r=i.useContext(s);return i.useMemo(function(){return"function"==typeof e?e(r):{...r,...e}},[r,e])}function c(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(t):e.components||t:o(e.components),i.createElement(s.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/f66b7bee.69fcab8e.js b/website/build/assets/js/f66b7bee.69fcab8e.js new file mode 100644 index 00000000..8f540afa --- /dev/null +++ b/website/build/assets/js/f66b7bee.69fcab8e.js @@ -0,0 +1 @@ +"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[9714],{95760(e,n,r){r.r(n),r.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>h,frontMatter:()=>t,metadata:()=>d,toc:()=>l});var i=r(86070),s=r(81753);const t={title:"Dependency Injection",sidebar_position:5,description:"How RCommon integrates with Microsoft.Extensions.DependencyInjection \u2014 service lifetimes, injecting repositories, IEventBus, ISystemTime, and named data stores."},o="Dependency Injection",d={id:"getting-started/dependency-injection",title:"Dependency Injection",description:"How RCommon integrates with Microsoft.Extensions.DependencyInjection \u2014 service lifetimes, injecting repositories, IEventBus, ISystemTime, and named data stores.",source:"@site/docs/getting-started/dependency-injection.mdx",sourceDirName:"getting-started",slug:"/getting-started/dependency-injection",permalink:"/docs/next/getting-started/dependency-injection",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/docs/getting-started/dependency-injection.mdx",tags:[],version:"current",sidebarPosition:5,frontMatter:{title:"Dependency Injection",sidebar_position:5,description:"How RCommon integrates with Microsoft.Extensions.DependencyInjection \u2014 service lifetimes, injecting repositories, IEventBus, ISystemTime, and named data stores."},sidebar:"docsSidebar",previous:{title:"Configuration & Bootstrapping",permalink:"/docs/next/getting-started/configuration"},next:{title:"Core Concepts",permalink:"/docs/next/category/core-concepts"}},c={},l=[{value:"How RCommon registers services",id:"how-rcommon-registers-services",level:2},{value:"Service lifetimes used by RCommon",id:"service-lifetimes-used-by-rcommon",level:2},{value:"Injecting RCommon services",id:"injecting-rcommon-services",level:2},{value:"Repositories",id:"repositories",level:3},{value:"IEventBus",id:"ieventbus",level:3},{value:"ISystemTime",id:"isystemtime",level:3},{value:"IGuidGenerator",id:"iguidgenerator",level:3},{value:"ICommonFactory<T>",id:"icommonfactoryt",level:3},{value:"Service registration patterns",id:"service-registration-patterns",level:2},{value:"Registering your own services alongside RCommon",id:"registering-your-own-services-alongside-rcommon",level:3},{value:"Named data stores",id:"named-data-stores",level:3},{value:"Multi-module bootstrapping",id:"multi-module-bootstrapping",level:2},{value:"Verifying registrations",id:"verifying-registrations",level:2}];function a(e){const n={a:"a",code:"code",h1:"h1",h2:"h2",h3:"h3",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,s.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"dependency-injection",children:"Dependency Injection"}),"\n",(0,i.jsxs)(n.p,{children:["RCommon is built entirely on ",(0,i.jsx)(n.code,{children:"Microsoft.Extensions.DependencyInjection"}),". It does not introduce its own container or wrap the standard one. Every service RCommon registers follows the same lifetime conventions you already know from ASP.NET Core."]}),"\n",(0,i.jsx)(n.h2,{id:"how-rcommon-registers-services",children:"How RCommon registers services"}),"\n",(0,i.jsxs)(n.p,{children:["When you call ",(0,i.jsx)(n.code,{children:"AddRCommon()"}),", the ",(0,i.jsx)(n.code,{children:"RCommonBuilder"})," receives the application's ",(0,i.jsx)(n.code,{children:"IServiceCollection"})," and calls ",(0,i.jsx)(n.code,{children:"AddSingleton"}),", ",(0,i.jsx)(n.code,{children:"AddScoped"}),", and ",(0,i.jsx)(n.code,{children:"AddTransient"})," on it directly. By the time ",(0,i.jsx)(n.code,{children:"builder.Build()"})," is called, all RCommon services are in the same container as your application services \u2014 no separate container, no child scope complications."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"// All of this is standard Microsoft DI\nvar builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddRCommon()\n .WithPersistence(ef => { ... })\n .WithMediator(m => { ... });\n\nvar app = builder.Build();\n"})}),"\n",(0,i.jsx)(n.h2,{id:"service-lifetimes-used-by-rcommon",children:"Service lifetimes used by RCommon"}),"\n",(0,i.jsx)(n.p,{children:"Understanding which lifetime each service uses helps you reason about when instances are created and shared."}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Service"}),(0,i.jsx)(n.th,{children:"Lifetime"}),(0,i.jsx)(n.th,{children:"Reason"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"EventSubscriptionManager"})}),(0,i.jsx)(n.td,{children:"Singleton"}),(0,i.jsx)(n.td,{children:"Subscription metadata is static; built once at startup"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"IEventBus"})," (",(0,i.jsx)(n.code,{children:"InMemoryEventBus"}),")"]}),(0,i.jsx)(n.td,{children:"Singleton"}),(0,i.jsx)(n.td,{children:"Stateless dispatcher; safe to share across requests"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"IEventRouter"})," (",(0,i.jsx)(n.code,{children:"InMemoryTransactionalEventRouter"}),")"]}),(0,i.jsx)(n.td,{children:"Scoped"}),(0,i.jsx)(n.td,{children:"Tied to the unit of work scope; one per HTTP request"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"IEntityEventTracker"})," (",(0,i.jsx)(n.code,{children:"InMemoryEntityEventTracker"}),")"]}),(0,i.jsx)(n.td,{children:"Scoped"}),(0,i.jsx)(n.td,{children:"Accumulates domain events during a single unit of work"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"ISubscriber"})," handler"]}),(0,i.jsx)(n.td,{children:"Scoped"}),(0,i.jsx)(n.td,{children:"Event handlers may depend on scoped services like repositories"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"ILinqRepository"})," / ",(0,i.jsx)(n.code,{children:"IReadOnlyRepository"})," / ",(0,i.jsx)(n.code,{children:"IWriteOnlyRepository"})]}),(0,i.jsx)(n.td,{children:"Transient"}),(0,i.jsx)(n.td,{children:"Stateless wrappers around the scoped DbContext"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IAggregateRepository"})}),(0,i.jsx)(n.td,{children:"Transient"}),(0,i.jsx)(n.td,{children:"Same as above"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IUnitOfWork"})}),(0,i.jsx)(n.td,{children:"Transient"}),(0,i.jsx)(n.td,{children:"New scope created each time a unit of work is opened"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IUnitOfWorkFactory"})}),(0,i.jsx)(n.td,{children:"Transient"}),(0,i.jsx)(n.td,{children:"Lightweight factory; safe as transient"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IDataStoreFactory"})}),(0,i.jsx)(n.td,{children:"Transient"}),(0,i.jsx)(n.td,{children:"Resolves the correct DbContext by name; delegates to the scoped DbContext"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"IGuidGenerator"})," (sequential)"]}),(0,i.jsx)(n.td,{children:"Transient"}),(0,i.jsx)(n.td,{children:"Stateless; new instance per call"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsxs)(n.td,{children:[(0,i.jsx)(n.code,{children:"IGuidGenerator"})," (simple)"]}),(0,i.jsx)(n.td,{children:"Scoped"}),(0,i.jsxs)(n.td,{children:["Registered as scoped by ",(0,i.jsx)(n.code,{children:"WithSimpleGuidGenerator"})]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ISystemTime"})}),(0,i.jsx)(n.td,{children:"Transient"}),(0,i.jsx)(n.td,{children:"Stateless clock wrapper"})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"ICommonFactory"})}),(0,i.jsx)(n.td,{children:"Scoped"}),(0,i.jsx)(n.td,{children:"Delegates to scoped service resolution"})]})]})]}),"\n",(0,i.jsx)(n.h2,{id:"injecting-rcommon-services",children:"Injecting RCommon services"}),"\n",(0,i.jsx)(n.h3,{id:"repositories",children:"Repositories"}),"\n",(0,i.jsxs)(n.p,{children:["After calling ",(0,i.jsx)(n.code,{children:"WithPersistence"}),", the open-generic repository interfaces are available for injection anywhere:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class OrderService\n{\n private readonly ILinqRepository _orders;\n\n public OrderService(ILinqRepository orders)\n {\n _orders = orders;\n }\n\n public async Task> GetPendingOrdersAsync()\n {\n return await _orders.FindAsync(o => o.Status == OrderStatus.Pending);\n }\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["The DI container resolves ",(0,i.jsx)(n.code,{children:"ILinqRepository"})," to ",(0,i.jsx)(n.code,{children:"EFCoreRepository"})," which in turn receives the scoped ",(0,i.jsx)(n.code,{children:"AppDbContext"})," through constructor injection."]}),"\n",(0,i.jsx)(n.h3,{id:"ieventbus",children:"IEventBus"}),"\n",(0,i.jsxs)(n.p,{children:["Inject ",(0,i.jsx)(n.code,{children:"IEventBus"})," wherever you need to publish events in-process. It is a singleton, so it can be injected into any lifetime."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class OrderApplicationService\n{\n private readonly ILinqRepository _orders;\n private readonly IEventBus _eventBus;\n\n public OrderApplicationService(\n ILinqRepository orders,\n IEventBus eventBus)\n {\n _orders = orders;\n _eventBus = eventBus;\n }\n\n public async Task PlaceOrderAsync(PlaceOrderRequest request)\n {\n var order = new Order(request.CustomerId, request.Items);\n await _orders.AddAsync(order);\n await _eventBus.PublishAsync(new OrderPlacedEvent(order.Id));\n }\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"isystemtime",children:"ISystemTime"}),"\n",(0,i.jsxs)(n.p,{children:["Inject ",(0,i.jsx)(n.code,{children:"ISystemTime"})," instead of calling ",(0,i.jsx)(n.code,{children:"DateTime.UtcNow"})," directly so that tests can control the clock:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class AuditService\n{\n private readonly ISystemTime _clock;\n\n public AuditService(ISystemTime clock)\n {\n _clock = clock;\n }\n\n public DateTime GetCurrentTime() => _clock.Now;\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"iguidgenerator",children:"IGuidGenerator"}),"\n",(0,i.jsxs)(n.p,{children:["Inject ",(0,i.jsx)(n.code,{children:"IGuidGenerator"})," to generate identifiers that are safe to use as primary keys:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"public class ProductFactory\n{\n private readonly IGuidGenerator _guidGenerator;\n\n public ProductFactory(IGuidGenerator guidGenerator)\n {\n _guidGenerator = guidGenerator;\n }\n\n public Product Create(string name, decimal price)\n {\n return new Product\n {\n Id = _guidGenerator.Create(),\n Name = name,\n Price = price\n };\n }\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"icommonfactoryt",children:"ICommonFactory"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"ICommonFactory"})," is a DI-aware factory that resolves instances of ",(0,i.jsx)(n.code,{children:"T"})," from the container on demand. Register it with ",(0,i.jsx)(n.code,{children:"WithCommonFactory"})," and then inject it where deferred or conditional resolution is needed:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"// Registration\nbuilder.Services.AddRCommon()\n .WithCommonFactory();\n\n// Usage\npublic class NotificationService\n{\n private readonly ICommonFactory _emailFactory;\n\n public NotificationService(ICommonFactory emailFactory)\n {\n _emailFactory = emailFactory;\n }\n\n public async Task SendAsync(string to, string subject, string body)\n {\n var sender = _emailFactory.Create();\n await sender.SendAsync(to, subject, body);\n }\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"service-registration-patterns",children:"Service registration patterns"}),"\n",(0,i.jsx)(n.h3,{id:"registering-your-own-services-alongside-rcommon",children:"Registering your own services alongside RCommon"}),"\n",(0,i.jsxs)(n.p,{children:["RCommon does not prevent you from registering services before or after ",(0,i.jsx)(n.code,{children:"AddRCommon()"}),". Standard registrations work exactly as expected:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"builder.Services.AddScoped();\n\nbuilder.Services.AddRCommon()\n .WithPersistence(ef => { ... });\n\nbuilder.Services.AddScoped();\n"})}),"\n",(0,i.jsx)(n.h3,{id:"named-data-stores",children:"Named data stores"}),"\n",(0,i.jsx)(n.p,{children:"When you register multiple DbContexts (for example a write model and a read model), each gets a name:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'.WithPersistence(ef =>\n{\n ef.AddDbContext("WriteDb", options =>\n options.UseSqlServer(writeConnectionString));\n\n ef.AddDbContext("ReadDb", options =>\n options.UseSqlServer(readConnectionString));\n\n ef.SetDefaultDataStore(ds =>\n ds.DefaultDataStoreName = "WriteDb");\n})\n'})}),"\n",(0,i.jsxs)(n.p,{children:["A repository can be directed to use a specific data store by setting ",(0,i.jsx)(n.code,{children:"DataStoreName"})," on the repository instance before use. The ",(0,i.jsx)(n.code,{children:"IDataStoreFactory"})," resolves the correct DbContext by name at runtime."]}),"\n",(0,i.jsx)(n.h2,{id:"multi-module-bootstrapping",children:"Multi-module bootstrapping"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"AddRCommon()"})," is safe to call from any number of modules in the same process. The first call constructs the ",(0,i.jsx)(n.code,{children:"IRCommonBuilder"})," and caches it on ",(0,i.jsx)(n.code,{children:"IServiceCollection"}),"; subsequent calls return the same instance, so all modules share one builder and one set of sub-builders. Three helper APIs support this pattern directly:"]}),"\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"API"}),(0,i.jsx)(n.th,{children:"Purpose"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IServiceCollection.IsRCommonInitialized()"})}),(0,i.jsxs)(n.td,{children:["Returns ",(0,i.jsx)(n.code,{children:"true"})," if any module in the process has already called ",(0,i.jsx)(n.code,{children:"AddRCommon()"}),". Useful for optional wiring in libraries that should skip themselves when RCommon is absent."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IRCommonBuilder.GetOrAddBuilder(Func)"})}),(0,i.jsxs)(n.td,{children:["The hook custom ",(0,i.jsx)(n.code,{children:"WithX"})," extensions use to opt into sub-builder caching. The factory runs at most once per ",(0,i.jsx)(n.code,{children:"TSubBuilder"})," per process."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"IRCommonBuilder.GetBootstrapDiagnostics()"})}),(0,i.jsxs)(n.td,{children:["After ",(0,i.jsx)(n.code,{children:"host.StartAsync()"}),", returns a report of soft duplicates detected by the descriptor scanner, or ",(0,i.jsx)(n.code,{children:"string.Empty"})," if nothing is suspicious."]})]})]})]}),"\n",(0,i.jsx)(n.p,{children:"A minimal module pattern:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:'public interface IServiceModule\n{\n void Configure(IServiceCollection services);\n}\n\npublic sealed class OrderingModule : IServiceModule\n{\n public void Configure(IServiceCollection services)\n {\n services.AddRCommon()\n .WithSimpleGuidGenerator()\n .WithPersistence(ef =>\n ef.AddDbContext(\n "Ordering",\n o => o.UseInMemoryDatabase("ordering")));\n }\n}\n\npublic sealed class InventoryModule : IServiceModule\n{\n public void Configure(IServiceCollection services)\n {\n services.AddRCommon()\n .WithSimpleGuidGenerator() // Same impl \u2014 idempotent no-op.\n .WithPersistence(ef =>\n ef.AddDbContext(\n "Inventory",\n o => o.UseInMemoryDatabase("inventory")));\n }\n}\n\n// Composition root\nvar host = Host.CreateDefaultBuilder(args)\n .ConfigureServices(services =>\n {\n IServiceModule[] modules = { new OrderingModule(), new InventoryModule() };\n foreach (var module in modules)\n module.Configure(services);\n })\n .Build();\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Both ",(0,i.jsx)(n.code,{children:"WithPersistence"})," calls share the same persistence sub-builder, so both ",(0,i.jsx)(n.code,{children:"AddDbContext"})," calls accumulate against one ",(0,i.jsx)(n.code,{children:"DataStoreFactoryOptions"}),". The duplicate ",(0,i.jsx)(n.code,{children:"WithSimpleGuidGenerator()"})," is a no-op because the impl type matches."]}),"\n",(0,i.jsxs)(n.p,{children:["See ",(0,i.jsx)(n.a,{href:"/docs/next/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix and ",(0,i.jsx)(n.code,{children:"Examples/Bootstrapping/Examples.Bootstrapping.MultiModule/"})," for a runnable three-module example."]}),"\n",(0,i.jsx)(n.h2,{id:"verifying-registrations",children:"Verifying registrations"}),"\n",(0,i.jsx)(n.p,{children:"During development you can dump the full service registration list to the console:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"Console.WriteLine(builder.Services.GenerateServiceDescriptorsString());\n"})}),"\n",(0,i.jsx)(n.p,{children:"To check for duplicate registrations \u2014 which can cause subtle bugs \u2014 use:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-csharp",children:"Console.WriteLine(builder.Services.GeneratePossibleDuplicatesServiceDescriptorsString());\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Both methods are extension methods on ",(0,i.jsx)(n.code,{children:"IServiceCollection"})," provided by ",(0,i.jsx)(n.code,{children:"RCommon.Core"}),"."]})]})}function h(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(a,{...e})}):a(e)}},81753(e,n,r){r.d(n,{R:()=>o,x:()=>d});var i=r(30758);const s={},t=i.createContext(s);function o(e){const n=i.useContext(t);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),i.createElement(t.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/website/build/assets/js/main.1cc4b96f.js b/website/build/assets/js/main.1cc4b96f.js new file mode 100644 index 00000000..f9949f01 --- /dev/null +++ b/website/build/assets/js/main.1cc4b96f.js @@ -0,0 +1,2 @@ +/*! For license information please see main.1cc4b96f.js.LICENSE.txt */ +(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[8792],{92310(e,t,n){var r={"./prism-bash":81022,"./prism-csharp":42739,"./prism-json":67874,"./prism-markup":69656};function a(e){var t=o(e);return n(t)}function o(e){if(!n.o(r,e)){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return r[e]}a.keys=function(){return Object.keys(r)},a.resolve=o,e.exports=a,a.id=92310},81345(e,t,n){"use strict";n.d(t,{A:()=>p});n(30758);var r=n(2415),a=n.n(r),o=n(84054);const i={"0058b4c6":[()=>n.e(849).then(n.t.bind(n,86164,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-175.json",86164],"0190ef14":[()=>Promise.all([n.e(1869),n.e(8995)]).then(n.bind(n,50478)),"@site/docs/event-handling/masstransit.mdx",50478],"0318d023":[()=>Promise.all([n.e(1869),n.e(6722)]).then(n.bind(n,50860)),"@site/versioned_docs/version-2.4.1/persistence/caching-redis.mdx",50860],"053d6f47":[()=>Promise.all([n.e(1869),n.e(7768)]).then(n.bind(n,81993)),"@site/docs/getting-started/quick-start.mdx",81993],"053f5a85":[()=>Promise.all([n.e(1869),n.e(1526)]).then(n.bind(n,6307)),"@site/versioned_docs/version-2.4.1/api-reference/migration-guide.mdx",6307],"05a98b77":[()=>Promise.all([n.e(1869),n.e(7376)]).then(n.bind(n,96004)),"@site/versioned_docs/version-2.4.1/architecture-guides/clean-architecture.mdx",96004],"0676c1eb":[()=>n.e(999).then(n.t.bind(n,7203,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-blob-storage-0a4.json",7203],"06c1478b":[()=>n.e(4096).then(n.bind(n,60089)),"@site/docs/api-reference/changelog.mdx",60089],"0728b9bc":[()=>n.e(1736).then(n.t.bind(n,53544,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-persistence-f9e.json",53544],"0806d8e0":[()=>Promise.all([n.e(1869),n.e(6174)]).then(n.bind(n,53933)),"@site/versioned_docs/version-2.4.1/cqrs-mediator/commands-handlers.mdx",53933],"0e171350":[()=>Promise.all([n.e(1869),n.e(2165)]).then(n.bind(n,1647)),"@site/docs/caching/memory.mdx",1647],"0ee8e78b":[()=>n.e(1047).then(n.bind(n,70795)),"@site/versioned_docs/version-2.4.1/messaging/overview.mdx",70795],"10134e3e":[()=>Promise.all([n.e(1869),n.e(9865)]).then(n.bind(n,46984)),"@site/docs/cqrs-mediator/mediatr.mdx",46984],"114ecb34":[()=>n.e(4006).then(n.bind(n,49637)),"@site/docs/state-machines/overview.mdx",49637],"12795a53":[()=>n.e(8176).then(n.t.bind(n,54739,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-caching-cec.json",54739],"138e0e15":[()=>n.e(4921).then(n.t.bind(n,41597,19)),"@generated/@easyops-cn/docusaurus-search-local/default/__plugin.json",41597],"14c60776":[()=>Promise.all([n.e(1869),n.e(4243)]).then(n.bind(n,9026)),"@site/versioned_docs/version-2.4.1/core-concepts/fluent-configuration.mdx",9026],"14eb3368":[()=>Promise.all([n.e(1869),n.e(6969)]).then(n.bind(n,30280)),"@theme/DocCategoryGeneratedIndexPage",30280],17896441:[()=>Promise.all([n.e(1869),n.e(5201),n.e(8401)]).then(n.bind(n,95142)),"@theme/DocItem",95142],"1900a2f2":[()=>n.e(688).then(n.bind(n,36806)),"@site/docs/getting-started/overview.mdx",36806],"195c7c19":[()=>Promise.all([n.e(1869),n.e(2617)]).then(n.bind(n,76542)),"@site/docs/validation/fluent-validation.mdx",76542],"1a4e3797":[()=>Promise.all([n.e(1869),n.e(2138)]).then(n.bind(n,40008)),"@theme/SearchPage",40008],"1d63fded":[()=>Promise.all([n.e(1869),n.e(4144)]).then(n.bind(n,83232)),"@site/versioned_docs/version-2.4.1/blob-storage/overview.mdx",83232],"1d75ee8e":[()=>Promise.all([n.e(1869),n.e(7031)]).then(n.bind(n,67384)),"@site/docs/api-reference/migration-guide.mdx",67384],"1dbca6ff":[()=>n.e(1528).then(n.t.bind(n,49236,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-api-reference-f49.json",49236],"1df93b7f":[()=>Promise.all([n.e(1869),n.e(4583)]).then(n.bind(n,87648)),"@site/src/pages/index.tsx",87648],"2069a4f9":[()=>n.e(6022).then(n.t.bind(n,96266,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-getting-started-191.json",96266],"210a241b":[()=>n.e(2874).then(n.bind(n,35866)),"@site/versioned_docs/version-2.4.1/api-reference/changelog.mdx",35866],21616783:[()=>Promise.all([n.e(1869),n.e(1685)]).then(n.bind(n,96167)),"@site/docs/cqrs-mediator/command-query-bus.mdx",96167],"217dc876":[()=>Promise.all([n.e(1869),n.e(8279)]).then(n.bind(n,37216)),"@site/docs/core-concepts/guid-generation.mdx",37216],"26c8eaff":[()=>Promise.all([n.e(1869),n.e(8568)]).then(n.bind(n,37099)),"@site/versioned_docs/version-2.4.1/examples-recipes/messaging.mdx",37099],"271208ba":[()=>n.e(7252).then(n.bind(n,49677)),"@site/versioned_docs/version-2.4.1/event-handling/overview.mdx",49677],"277e8678":[()=>Promise.all([n.e(1869),n.e(8621)]).then(n.bind(n,65300)),"@site/docs/blob-storage/s3.mdx",65300],"28aaf32e":[()=>Promise.all([n.e(1869),n.e(5349)]).then(n.bind(n,34483)),"@site/versioned_docs/version-2.4.1/persistence/linq2db.mdx",34483],"290deaa0":[()=>n.e(305).then(n.bind(n,78310)),"@site/versioned_docs/version-2.4.1/getting-started/configuration.mdx",78310],"2952a4a2":[()=>n.e(815).then(n.t.bind(n,65528,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-examples-recipes-ad3.json",65528],"2b9a25fb":[()=>Promise.all([n.e(1869),n.e(7682)]).then(n.bind(n,83397)),"@site/docs/event-handling/mediatr.mdx",83397],"2c2268a5":[()=>Promise.all([n.e(1869),n.e(7486)]).then(n.bind(n,24631)),"@site/docs/security-web/web-utilities.mdx",24631],"2d7572c8":[()=>n.e(4483).then(n.t.bind(n,27615,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-email-279.json",27615],"2fb564d3":[()=>Promise.all([n.e(1869),n.e(1909)]).then(n.bind(n,14667)),"@site/docs/cqrs-mediator/wolverine.mdx",14667],"30d27832":[()=>n.e(4890).then(n.bind(n,12397)),"@site/docs/getting-started/configuration.mdx",12397],"3249bc72":[()=>n.e(6827).then(n.t.bind(n,90993,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-security-web-bc4.json",90993],"364ef54b":[()=>Promise.all([n.e(1869),n.e(2704)]).then(n.bind(n,52054)),"@site/docs/examples-recipes/hr-leave-management.mdx",52054],"36706c18":[()=>n.e(575).then(n.bind(n,80481)),"@site/versioned_docs/version-2.4.1/index.mdx",80481],"36d4d2ec":[()=>Promise.all([n.e(1869),n.e(9050)]).then(n.bind(n,35703)),"@site/docs/domain-driven-design/value-objects.mdx",35703],"3724cf18":[()=>n.e(4635).then(n.t.bind(n,93353,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-email-ef9.json",93353],37382023:[()=>Promise.all([n.e(1869),n.e(1511)]).then(n.bind(n,7581)),"@site/docs/messaging/state-machines.mdx",7581],"3833ae79":[()=>Promise.all([n.e(1869),n.e(6970)]).then(n.bind(n,10191)),"@site/versioned_docs/version-2.4.1/cqrs-mediator/mediatr.mdx",10191],"398d0abe":[()=>Promise.all([n.e(1869),n.e(1053)]).then(n.bind(n,44952)),"@site/versioned_docs/version-2.4.1/persistence/caching-memory.mdx",44952],"3a32ccad":[()=>Promise.all([n.e(1869),n.e(1861)]).then(n.bind(n,40739)),"@site/versioned_docs/version-2.4.1/cqrs-mediator/queries-handlers.mdx",40739],"3a91dea9":[()=>Promise.all([n.e(1869),n.e(4506)]).then(n.bind(n,51535)),"@site/versioned_docs/version-2.4.1/multi-tenancy/overview.mdx",51535],"3ae51d95":[()=>Promise.all([n.e(1869),n.e(9080)]).then(n.bind(n,7407)),"@site/versioned_docs/version-2.4.1/core-concepts/guards.mdx",7407],"3ba522eb":[()=>Promise.all([n.e(1869),n.e(2227)]).then(n.bind(n,58220)),"@site/versioned_docs/version-2.4.1/persistence/repository-pattern.mdx",58220],"3bee93a3":[()=>Promise.all([n.e(1869),n.e(2298)]).then(n.bind(n,83359)),"@site/versioned_docs/version-2.4.1/testing/overview.mdx",83359],"3c9b5e9c":[()=>n.e(8994).then(n.t.bind(n,35002,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-multi-tenancy-f52.json",35002],"3cd55c21":[()=>Promise.all([n.e(1869),n.e(5531)]).then(n.bind(n,32826)),"@site/docs/examples-recipes/messaging.mdx",32826],"3df138ae":[()=>n.e(4994).then(n.bind(n,49103)),"@site/docs/caching/overview.mdx",49103],"3f83c19f":[()=>Promise.all([n.e(1869),n.e(3534)]).then(n.bind(n,65014)),"@site/versioned_docs/version-2.4.1/domain-driven-design/value-objects.mdx",65014],"455b3ac1":[()=>n.e(3095).then(n.t.bind(n,44770,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-validation-514.json",44770],"456fddd5":[()=>Promise.all([n.e(1869),n.e(2675)]).then(n.bind(n,15243)),"@site/versioned_docs/version-2.4.1/serialization/newtonsoft.mdx",15243],"462845a6":[()=>Promise.all([n.e(1869),n.e(5999)]).then(n.bind(n,42722)),"@site/docs/architecture-guides/microservices.mdx",42722],"4aa4dbe5":[()=>Promise.all([n.e(1869),n.e(4061)]).then(n.bind(n,59754)),"@site/versioned_docs/version-2.4.1/architecture-guides/event-driven.mdx",59754],"4ad18091":[()=>n.e(4125).then(n.t.bind(n,63545,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-architecture-guides-bbe.json",63545],"4edc808e":[()=>n.e(308).then(n.bind(n,93006)),"@site/docs/index.mdx",93006],"51c0b4e0":[()=>Promise.all([n.e(1869),n.e(7953)]).then(n.bind(n,70600)),"@site/versioned_docs/version-2.4.1/domain-driven-design/domain-events.mdx",70600],"51d7c70d":[()=>n.e(940).then(n.t.bind(n,64127,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-state-machines-204.json",64127],"51ddfa24":[()=>Promise.all([n.e(1869),n.e(4444)]).then(n.bind(n,71505)),"@site/docs/architecture-guides/clean-architecture.mdx",71505],"523cdb73":[()=>Promise.all([n.e(1869),n.e(323)]).then(n.bind(n,97930)),"@site/docs/multi-tenancy/overview.mdx",97930],"5338c7f2":[()=>Promise.all([n.e(1869),n.e(2924)]).then(n.bind(n,18892)),"@site/docs/event-handling/transactional-outbox.mdx",18892],"5591a79c":[()=>n.e(2007).then(n.t.bind(n,22274,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-messaging-463.json",22274],"58715ff3":[()=>Promise.all([n.e(1869),n.e(6488)]).then(n.bind(n,11560)),"@site/docs/event-handling/in-memory.mdx",11560],"59a18301":[()=>Promise.all([n.e(1869),n.e(8213)]).then(n.bind(n,35045)),"@site/docs/persistence/repository-pattern.mdx",35045],"5a4fc6f7":[()=>Promise.all([n.e(1869),n.e(6581)]).then(n.bind(n,62103)),"@site/docs/state-machines/stateless.mdx",62103],"5bf2758b":[()=>n.e(7692).then(n.t.bind(n,85621,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-state-machines-983.json",85621],"5ddc5782":[()=>Promise.all([n.e(1869),n.e(6721)]).then(n.bind(n,68269)),"@site/versioned_docs/version-2.4.1/messaging/wolverine.mdx",68269],"5dec1a30":[()=>n.e(3007).then(n.bind(n,23281)),"@site/versioned_docs/version-2.4.1/event-handling/distributed.mdx",23281],"5e6bb026":[()=>Promise.all([n.e(1869),n.e(4103)]).then(n.bind(n,30992)),"@site/docs/persistence/efcore.mdx",30992],"5e95c892":[()=>n.e(9647).then(n.bind(n,73490)),"@theme/DocsRoot",73490],"5e9f5e1a":[()=>Promise.resolve().then(n.bind(n,4784)),"@generated/docusaurus.config",4784],"5f236a12":[()=>Promise.all([n.e(1869),n.e(5493)]).then(n.bind(n,59425)),"@site/docs/caching/redis.mdx",59425],"61649f5a":[()=>n.e(8193).then(n.bind(n,68188)),"@site/docs/event-handling/distributed.mdx",68188],"61c074c7":[()=>Promise.all([n.e(1869),n.e(7894)]).then(n.bind(n,97190)),"@site/docs/serialization/overview.mdx",97190],"63add8cf":[()=>n.e(6962).then(n.t.bind(n,41948,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-testing-d89.json",41948],"6459b84b":[()=>Promise.all([n.e(1869),n.e(6459)]).then(n.bind(n,39915)),"@site/docs/getting-started/installation.mdx",39915],65924626:[()=>n.e(7131).then(n.t.bind(n,54397,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-domain-driven-design-466.json",54397],"66ee7627":[()=>Promise.all([n.e(1869),n.e(5839)]).then(n.bind(n,25099)),"@site/docs/persistence/caching-redis.mdx",25099],"6769fb4c":[()=>n.e(2726).then(n.t.bind(n,36134,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-api-reference-692.json",36134],"6840d99a":[()=>n.e(7426).then(n.t.bind(n,46926,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-architecture-guides-183.json",46926],"6c7fac80":[()=>Promise.all([n.e(1869),n.e(1305)]).then(n.bind(n,65486)),"@site/versioned_docs/version-2.4.1/getting-started/installation.mdx",65486],"6cfa6dfb":[()=>Promise.all([n.e(1869),n.e(6743)]).then(n.bind(n,25581)),"@site/docs/security-web/authorization.mdx",25581],"6d12448e":[()=>Promise.all([n.e(1869),n.e(5949)]).then(n.bind(n,78422)),"@site/docs/persistence/unit-of-work.mdx",78422],"6d2c3570":[()=>Promise.all([n.e(1869),n.e(5309)]).then(n.bind(n,68037)),"@site/docs/persistence/caching-memory.mdx",68037],"6e4c0b34":[()=>n.e(796).then(n.t.bind(n,46929,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-testing-2e9.json",46929],"700dc998":[()=>Promise.all([n.e(1869),n.e(3089)]).then(n.bind(n,15549)),"@site/versioned_docs/version-2.4.1/messaging/masstransit.mdx",15549],"706a23ff":[()=>n.e(5088).then(n.bind(n,45575)),"@site/versioned_docs/version-2.4.1/getting-started/overview.mdx",45575],"72caafa9":[()=>Promise.all([n.e(1869),n.e(1375)]).then(n.bind(n,47232)),"@site/versioned_docs/version-2.4.1/security-web/authorization.mdx",47232],"730b3122":[()=>n.e(7395).then(n.t.bind(n,28588,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-core-concepts-d9c.json",28588],"759c0ecd":[()=>Promise.all([n.e(1869),n.e(3271)]).then(n.bind(n,65800)),"@site/versioned_docs/version-2.4.1/messaging/state-machines.mdx",65800],"7746528f":[()=>Promise.all([n.e(1869),n.e(1416)]).then(n.bind(n,34101)),"@site/versioned_docs/version-2.4.1/event-handling/in-memory.mdx",34101],78638370:[()=>Promise.all([n.e(1869),n.e(3705)]).then(n.bind(n,16)),"@site/docs/messaging/transactional-outbox.mdx",16],"7d8ddf48":[()=>Promise.all([n.e(1869),n.e(1880)]).then(n.bind(n,91543)),"@site/docs/serialization/system-text-json.mdx",91543],"80e32187":[()=>Promise.all([n.e(1869),n.e(9271)]).then(n.bind(n,73871)),"@site/versioned_docs/version-2.4.1/blob-storage/s3.mdx",73871],"8110047e":[()=>Promise.all([n.e(1869),n.e(8389)]).then(n.bind(n,92254)),"@site/docs/api-reference/nuget-packages.mdx",92254],"8295b30b":[()=>n.e(2180).then(n.bind(n,41506)),"@site/versioned_docs/version-2.4.1/caching/overview.mdx",41506],"836414ee":[()=>Promise.all([n.e(1869),n.e(967)]).then(n.bind(n,53908)),"@site/versioned_docs/version-2.4.1/email/sendgrid.mdx",53908],"843aa858":[()=>n.e(1379).then(n.bind(n,41801)),"@site/docs/persistence/sagas.mdx",41801],"8a13d96c":[()=>Promise.all([n.e(1869),n.e(2042)]).then(n.bind(n,36471)),"@site/docs/blob-storage/overview.mdx",36471],"8adc58e7":[()=>Promise.all([n.e(1869),n.e(7958)]).then(n.bind(n,2451)),"@site/versioned_docs/version-2.4.1/api-reference/nuget-packages.mdx",2451],"8b219ab3":[()=>Promise.all([n.e(1869),n.e(3709)]).then(n.bind(n,50631)),"@site/docs/architecture-guides/event-driven.mdx",50631],"8bb34e3d":[()=>n.e(6712).then(n.t.bind(n,98917,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-multi-tenancy-b19.json",98917],"8f4ccb68":[()=>Promise.all([n.e(1869),n.e(2479)]).then(n.bind(n,20702)),"@site/docs/persistence/specifications.mdx",20702],"90bfbd1a":[()=>n.e(2056).then(n.t.bind(n,56022,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-examples-recipes-c23.json",56022],"92e4e614":[()=>n.e(1531).then(n.t.bind(n,4571,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-validation-f5f.json",4571],"955ba9c4":[()=>Promise.all([n.e(1869),n.e(5835)]).then(n.bind(n,71091)),"@site/versioned_docs/version-2.4.1/core-concepts/guid-generation.mdx",71091],"9844f34b":[()=>Promise.all([n.e(1869),n.e(2771)]).then(n.bind(n,31682)),"@site/versioned_docs/version-2.4.1/caching/redis.mdx",31682],"98e9a02b":[()=>Promise.all([n.e(1869),n.e(4629)]).then(n.bind(n,25919)),"@site/versioned_docs/version-2.4.1/validation/fluent-validation.mdx",25919],"9d132284":[()=>n.e(7688).then(n.t.bind(n,41568,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-core-concepts-fa7.json",41568],"9e0ce23f":[()=>Promise.all([n.e(1869),n.e(7912)]).then(n.bind(n,41827)),"@site/docs/domain-driven-design/soft-delete.mdx",41827],"9ec94fa2":[()=>n.e(3522).then(n.bind(n,87586)),"@site/docs/event-handling/overview.mdx",87586],"9fbceb30":[()=>Promise.all([n.e(1869),n.e(1759)]).then(n.bind(n,31358)),"@site/versioned_docs/version-2.4.1/getting-started/quick-start.mdx",31358],a00295fc:[()=>Promise.all([n.e(1869),n.e(3325)]).then(n.bind(n,1638)),"@site/docs/messaging/wolverine.mdx",1638],a0bf6e44:[()=>Promise.all([n.e(1869),n.e(4083)]).then(n.bind(n,24514)),"@site/versioned_docs/version-2.4.1/core-concepts/system-time.mdx",24514],a250565b:[()=>Promise.all([n.e(1869),n.e(8901)]).then(n.bind(n,30338)),"@site/versioned_docs/version-2.4.1/caching/memory.mdx",30338],a283db28:[()=>Promise.all([n.e(1869),n.e(5585)]).then(n.bind(n,99870)),"@site/docs/email/overview.mdx",99870],a6456b1a:[()=>Promise.all([n.e(1869),n.e(8437)]).then(n.bind(n,63269)),"@site/versioned_docs/version-2.4.1/examples-recipes/event-handling.mdx",63269],a7456010:[()=>n.e(1235).then(n.t.bind(n,88552,19)),"@generated/docusaurus-plugin-content-pages/default/__plugin.json",88552],a7bd4aaa:[()=>n.e(7098).then(n.bind(n,40831)),"@theme/DocVersionRoot",40831],a94703ab:[()=>Promise.all([n.e(1869),n.e(9048)]).then(n.bind(n,32190)),"@theme/DocRoot",32190],aa6ee3ba:[()=>Promise.all([n.e(1869),n.e(7769)]).then(n.bind(n,81478)),"@site/versioned_docs/version-2.4.1/state-machines/stateless.mdx",81478],aacb6ba3:[()=>Promise.all([n.e(1869),n.e(5162)]).then(n.bind(n,40166)),"@site/docs/cqrs-mediator/queries-handlers.mdx",40166],aba21aa0:[()=>n.e(5742).then(n.t.bind(n,27093,19)),"@generated/docusaurus-plugin-content-docs/default/__plugin.json",27093],ac58a566:[()=>Promise.all([n.e(1869),n.e(2362)]).then(n.bind(n,52380)),"@site/versioned_docs/version-2.4.1/cqrs-mediator/wolverine.mdx",52380],af358a8e:[()=>Promise.all([n.e(1869),n.e(1262)]).then(n.bind(n,60222)),"@site/docs/serialization/newtonsoft.mdx",60222],afa07236:[()=>Promise.all([n.e(1869),n.e(641)]).then(n.bind(n,32082)),"@site/docs/messaging/masstransit.mdx",32082],b0a26e2e:[()=>Promise.all([n.e(1869),n.e(1048)]).then(n.bind(n,69526)),"@site/docs/cqrs-mediator/commands-handlers.mdx",69526],b12f6cde:[()=>Promise.all([n.e(1869),n.e(4724)]).then(n.bind(n,10798)),"@site/versioned_docs/version-2.4.1/domain-driven-design/soft-delete.mdx",10798],b1929690:[()=>Promise.all([n.e(1869),n.e(4959)]).then(n.bind(n,14087)),"@site/docs/core-concepts/fluent-configuration.mdx",14087],b1d2e6ae:[()=>Promise.all([n.e(1869),n.e(8321)]).then(n.bind(n,25228)),"@site/versioned_docs/version-2.4.1/blob-storage/azure.mdx",25228],b39b6a55:[()=>Promise.all([n.e(1869),n.e(7190)]).then(n.bind(n,20795)),"@site/versioned_docs/version-2.4.1/domain-driven-design/auditing.mdx",20795],b3e438ca:[()=>n.e(8256).then(n.t.bind(n,16864,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-cqrs-mediator-152.json",16864],b402cf60:[()=>Promise.all([n.e(1869),n.e(6626)]).then(n.bind(n,65215)),"@site/versioned_docs/version-2.4.1/persistence/unit-of-work.mdx",65215],b78116b8:[()=>Promise.all([n.e(1869),n.e(9144)]).then(n.bind(n,38325)),"@site/versioned_docs/version-2.4.1/messaging/transactional-outbox.mdx",38325],b8f37394:[()=>Promise.all([n.e(1869),n.e(4573)]).then(n.bind(n,56952)),"@site/docs/persistence/linq2db.mdx",56952],bc9574b1:[()=>Promise.all([n.e(1869),n.e(9846)]).then(n.bind(n,99667)),"@site/versioned_docs/version-2.4.1/examples-recipes/hr-leave-management.mdx",99667],bc95c22b:[()=>n.e(8647).then(n.t.bind(n,62903,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-persistence-90f.json",62903],be990863:[()=>n.e(867).then(n.t.bind(n,60015,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-serialization-206.json",60015],bf1307fc:[()=>n.e(9525).then(n.t.bind(n,31413,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-d71.json",31413],c260b502:[()=>n.e(635).then(n.t.bind(n,78728,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-getting-started-da0.json",78728],c2e5e2c0:[()=>Promise.all([n.e(1869),n.e(686)]).then(n.bind(n,14493)),"@site/versioned_docs/version-2.4.1/persistence/dapper.mdx",14493],c4817ddd:[()=>Promise.all([n.e(1869),n.e(8346)]).then(n.bind(n,1196)),"@site/docs/persistence/dapper.mdx",1196],c4c9165d:[()=>Promise.all([n.e(1869),n.e(2472)]).then(n.bind(n,78258)),"@site/docs/testing/overview.mdx",78258],c6c4f386:[()=>Promise.all([n.e(1869),n.e(8442)]).then(n.bind(n,13693)),"@site/docs/core-concepts/execution-results.mdx",13693],c8b194bd:[()=>Promise.all([n.e(1869),n.e(4308)]).then(n.bind(n,72709)),"@site/docs/blob-storage/azure.mdx",72709],cea878c3:[()=>n.e(67).then(n.t.bind(n,69401,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-event-handling-093.json",69401],d41de234:[()=>Promise.all([n.e(1869),n.e(7760)]).then(n.bind(n,84319)),"@site/versioned_docs/version-2.4.1/event-handling/transactional-outbox.mdx",84319],d71aaf87:[()=>n.e(4360).then(n.t.bind(n,23754,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-blob-storage-1ad.json",23754],d7ba9c2b:[()=>Promise.all([n.e(1869),n.e(1475)]).then(n.bind(n,95550)),"@site/docs/core-concepts/guards.mdx",95550],da584be8:[()=>Promise.all([n.e(1869),n.e(584)]).then(n.bind(n,68218)),"@site/versioned_docs/version-2.4.1/security-web/web-utilities.mdx",68218],da98a4e0:[()=>n.e(6165).then(n.t.bind(n,57833,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-serialization-df2.json",57833],dbaa9460:[()=>Promise.all([n.e(1869),n.e(9330)]).then(n.bind(n,25376)),"@site/versioned_docs/version-2.4.1/event-handling/mediatr.mdx",25376],dcbbd3c4:[()=>n.e(8770).then(n.bind(n,57913)),"@site/versioned_docs/version-2.4.1/state-machines/overview.mdx",57913],de3ef6f7:[()=>Promise.all([n.e(1869),n.e(3991)]).then(n.bind(n,83262)),"@site/docs/examples-recipes/event-handling.mdx",83262],e0146154:[()=>Promise.all([n.e(1869),n.e(139)]).then(n.bind(n,19306)),"@site/docs/event-handling/wolverine.mdx",19306],e088aa64:[()=>Promise.all([n.e(1869),n.e(3012)]).then(n.bind(n,10915)),"@site/versioned_docs/version-2.4.1/persistence/specifications.mdx",10915],e1418cd9:[()=>n.e(850).then(n.t.bind(n,99661,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-event-handling-57b.json",99661],e1da346e:[()=>Promise.all([n.e(1869),n.e(1490)]).then(n.bind(n,78612)),"@site/versioned_docs/version-2.4.1/cqrs-mediator/command-query-bus.mdx",78612],e3b543e0:[()=>Promise.all([n.e(1869),n.e(8764)]).then(n.bind(n,51773)),"@site/versioned_docs/version-2.4.1/architecture-guides/microservices.mdx",51773],e3ba908e:[()=>n.e(7134).then(n.bind(n,60485)),"@site/docs/core-concepts/modular-composition.mdx",60485],e3d4b007:[()=>n.e(9774).then(n.t.bind(n,91378,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-caching-248.json",91378],e592b2d8:[()=>Promise.all([n.e(1869),n.e(8048)]).then(n.bind(n,56357)),"@site/docs/domain-driven-design/domain-events.mdx",56357],e59bb016:[()=>Promise.all([n.e(1869),n.e(8599)]).then(n.bind(n,14897)),"@site/docs/core-concepts/system-time.mdx",14897],e629df9e:[()=>Promise.all([n.e(1869),n.e(549)]).then(n.bind(n,37609)),"@site/docs/examples-recipes/caching.mdx",37609],e74d8daa:[()=>n.e(3041).then(n.t.bind(n,4974,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-messaging-4a0.json",4974],e828455b:[()=>Promise.all([n.e(1869),n.e(9186)]).then(n.bind(n,3637)),"@site/versioned_docs/version-2.4.1/testing/test-base-classes.mdx",3637],e8bb775d:[()=>Promise.all([n.e(1869),n.e(7009)]).then(n.bind(n,37867)),"@site/versioned_docs/version-2.4.1/event-handling/masstransit.mdx",37867],ea106b39:[()=>n.e(7858).then(n.bind(n,89262)),"@site/docs/messaging/overview.mdx",89262],ea8a8189:[()=>n.e(8192).then(n.t.bind(n,1300,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-cqrs-mediator-6d0.json",1300],ec266f48:[()=>Promise.all([n.e(1869),n.e(3915)]).then(n.bind(n,22204)),"@site/versioned_docs/version-2.4.1/domain-driven-design/entities-aggregates.mdx",22204],ed1b6b94:[()=>Promise.all([n.e(1869),n.e(7170)]).then(n.bind(n,2160)),"@site/docs/domain-driven-design/auditing.mdx",2160],ed2de094:[()=>Promise.all([n.e(1869),n.e(5358)]).then(n.bind(n,91490)),"@site/docs/testing/test-base-classes.mdx",91490],ed7a6cd0:[()=>Promise.all([n.e(1869),n.e(189)]).then(n.bind(n,69343)),"@site/versioned_docs/version-2.4.1/serialization/overview.mdx",69343],edfd1898:[()=>n.e(1162).then(n.bind(n,82138)),"@site/versioned_docs/version-2.4.1/persistence/sagas.mdx",82138],eed12734:[()=>Promise.all([n.e(1869),n.e(8740)]).then(n.bind(n,10938)),"@site/versioned_docs/version-2.4.1/serialization/system-text-json.mdx",10938],eeda89c2:[()=>Promise.all([n.e(1869),n.e(834)]).then(n.bind(n,63427)),"@site/versioned_docs/version-2.4.1/event-handling/wolverine.mdx",63427],ef3de7f1:[()=>Promise.all([n.e(1869),n.e(312)]).then(n.bind(n,50262)),"@site/versioned_docs/version-2.4.1/core-concepts/execution-results.mdx",50262],ef4bc3e4:[()=>Promise.all([n.e(1869),n.e(8733)]).then(n.bind(n,54645)),"@site/docs/domain-driven-design/entities-aggregates.mdx",54645],f0840d59:[()=>Promise.all([n.e(1869),n.e(9315)]).then(n.bind(n,44393)),"@site/docs/email/sendgrid.mdx",44393],f57362ea:[()=>Promise.all([n.e(1869),n.e(768)]).then(n.bind(n,41765)),"@site/versioned_docs/version-2.4.1/persistence/efcore.mdx",41765],f66b7bee:[()=>n.e(9714).then(n.bind(n,95760)),"@site/docs/getting-started/dependency-injection.mdx",95760],f6bf7b54:[()=>Promise.all([n.e(1869),n.e(3555)]).then(n.bind(n,4448)),"@site/docs/multi-tenancy/finbuckle.mdx",4448],f6fe64e5:[()=>Promise.all([n.e(1869),n.e(5908)]).then(n.bind(n,38024)),"@site/versioned_docs/version-2.4.1/examples-recipes/caching.mdx",38024],f8d477c9:[()=>n.e(2183).then(n.bind(n,4593)),"@site/versioned_docs/version-2.4.1/getting-started/dependency-injection.mdx",4593],f980f00b:[()=>Promise.all([n.e(1869),n.e(4568)]).then(n.bind(n,355)),"@site/versioned_docs/version-2.4.1/multi-tenancy/finbuckle.mdx",355],fa7d3bf6:[()=>n.e(6334).then(n.t.bind(n,42467,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-next-category-security-web-a73.json",42467],fbd1c788:[()=>Promise.all([n.e(1869),n.e(2076)]).then(n.bind(n,20047)),"@site/versioned_docs/version-2.4.1/email/overview.mdx",20047],ff18c739:[()=>n.e(4806).then(n.t.bind(n,14724,19)),"@generated/docusaurus-plugin-content-docs/default/p/docs-category-domain-driven-design-e2b.json",14724]};var s=n(86070);function c({error:e,retry:t,pastDelay:n}){return e?(0,s.jsxs)("div",{style:{textAlign:"center",color:"#fff",backgroundColor:"#fa383e",borderColor:"#fa383e",borderStyle:"solid",borderRadius:"0.25rem",borderWidth:"1px",boxSizing:"border-box",display:"block",padding:"1rem",flex:"0 0 50%",marginLeft:"25%",marginRight:"25%",marginTop:"5rem",maxWidth:"50%",width:"100%"},children:[(0,s.jsx)("p",{children:String(e)}),(0,s.jsx)("div",{children:(0,s.jsx)("button",{type:"button",onClick:t,children:"Retry"})})]}):n?(0,s.jsx)("div",{style:{display:"flex",justifyContent:"center",alignItems:"center",height:"100vh"},children:(0,s.jsx)("svg",{id:"loader",style:{width:128,height:110,position:"absolute",top:"calc(100vh - 64%)"},viewBox:"0 0 45 45",xmlns:"http://www.w3.org/2000/svg",stroke:"#61dafb",children:(0,s.jsxs)("g",{fill:"none",fillRule:"evenodd",transform:"translate(1 1)",strokeWidth:"2",children:[(0,s.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,s.jsx)("animate",{attributeName:"r",begin:"1.5s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,s.jsx)("animate",{attributeName:"stroke-opacity",begin:"1.5s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,s.jsx)("animate",{attributeName:"stroke-width",begin:"1.5s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,s.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,s.jsx)("animate",{attributeName:"r",begin:"3s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,s.jsx)("animate",{attributeName:"stroke-opacity",begin:"3s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,s.jsx)("animate",{attributeName:"stroke-width",begin:"3s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,s.jsx)("circle",{cx:"22",cy:"22",r:"8",children:(0,s.jsx)("animate",{attributeName:"r",begin:"0s",dur:"1.5s",values:"6;1;2;3;4;5;6",calcMode:"linear",repeatCount:"indefinite"})})]})})}):null}var l=n(73946),u=n(65777);function d(e,t){if("*"===e)return a()({loading:c,loader:()=>n.e(6410).then(n.bind(n,46410)),modules:["@theme/NotFound"],webpack:()=>[46410],render(e,t){const n=e.default;return(0,s.jsx)(u.W,{value:{plugin:{name:"native",id:"default"}},children:(0,s.jsx)(n,{...t})})}});const r=o[`${e}-${t}`],d={},p=[],f=[],g=(0,l.A)(r);return Object.entries(g).forEach(([e,t])=>{const n=i[t];n&&(d[e]=n[0],p.push(n[1]),f.push(n[2]))}),a().Map({loading:c,loader:d,modules:p,webpack:()=>f,render(t,n){const a=JSON.parse(JSON.stringify(r));Object.entries(t).forEach(([t,n])=>{const r=n.default;if(!r)throw new Error(`The page component at ${e} doesn't have a default export. This makes it impossible to render anything. Consider default-exporting a React component.`);"object"!=typeof r&&"function"!=typeof r||Object.keys(n).filter(e=>"default"!==e).forEach(e=>{r[e]=n[e]});let o=a;const i=t.split(".");i.slice(0,-1).forEach(e=>{o=o[e]}),o[i[i.length-1]]=r});const o=a.__comp;delete a.__comp;const i=a.__context;delete a.__context;const c=a.__props;return delete a.__props,(0,s.jsx)(u.W,{value:i,children:(0,s.jsx)(o,{...a,...c,...n})})}})}const p=[{path:"/search",component:d("/search","822"),exact:!0},{path:"/docs",component:d("/docs","732"),routes:[{path:"/docs/next",component:d("/docs/next","1a8"),routes:[{path:"/docs/next",component:d("/docs/next","bf6"),routes:[{path:"/docs/next",component:d("/docs/next","c40"),exact:!0},{path:"/docs/next/api-reference/changelog",component:d("/docs/next/api-reference/changelog","b56"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/api-reference/migration-guide",component:d("/docs/next/api-reference/migration-guide","142"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/api-reference/nuget-packages",component:d("/docs/next/api-reference/nuget-packages","537"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/architecture-guides/clean-architecture",component:d("/docs/next/architecture-guides/clean-architecture","b73"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/architecture-guides/event-driven",component:d("/docs/next/architecture-guides/event-driven","d41"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/architecture-guides/microservices",component:d("/docs/next/architecture-guides/microservices","317"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/blob-storage/azure",component:d("/docs/next/blob-storage/azure","6c3"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/blob-storage/overview",component:d("/docs/next/blob-storage/overview","4db"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/blob-storage/s3",component:d("/docs/next/blob-storage/s3","b75"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/caching/memory",component:d("/docs/next/caching/memory","a65"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/caching/overview",component:d("/docs/next/caching/overview","43e"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/caching/redis",component:d("/docs/next/caching/redis","a6d"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/api-reference",component:d("/docs/next/category/api-reference","824"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/architecture-guides",component:d("/docs/next/category/architecture-guides","53e"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/blob-storage",component:d("/docs/next/category/blob-storage","a18"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/caching",component:d("/docs/next/category/caching","45f"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/core-concepts",component:d("/docs/next/category/core-concepts","206"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/cqrs--mediator",component:d("/docs/next/category/cqrs--mediator","8f5"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/domain-driven-design",component:d("/docs/next/category/domain-driven-design","91e"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/email",component:d("/docs/next/category/email","b81"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/event-handling",component:d("/docs/next/category/event-handling","6ae"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/examples--recipes",component:d("/docs/next/category/examples--recipes","571"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/getting-started",component:d("/docs/next/category/getting-started","fac"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/messaging",component:d("/docs/next/category/messaging","ca4"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/multi-tenancy",component:d("/docs/next/category/multi-tenancy","302"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/persistence",component:d("/docs/next/category/persistence","b01"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/security--web",component:d("/docs/next/category/security--web","ef7"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/serialization",component:d("/docs/next/category/serialization","0fd"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/state-machines",component:d("/docs/next/category/state-machines","87b"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/testing",component:d("/docs/next/category/testing","49c"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/category/validation",component:d("/docs/next/category/validation","2c1"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/core-concepts/execution-results",component:d("/docs/next/core-concepts/execution-results","78c"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/core-concepts/fluent-configuration",component:d("/docs/next/core-concepts/fluent-configuration","f58"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/core-concepts/guards",component:d("/docs/next/core-concepts/guards","153"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/core-concepts/guid-generation",component:d("/docs/next/core-concepts/guid-generation","df6"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/core-concepts/modular-composition",component:d("/docs/next/core-concepts/modular-composition","40a"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/core-concepts/system-time",component:d("/docs/next/core-concepts/system-time","1a0"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/cqrs-mediator/command-query-bus",component:d("/docs/next/cqrs-mediator/command-query-bus","4f6"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/cqrs-mediator/commands-handlers",component:d("/docs/next/cqrs-mediator/commands-handlers","f87"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/cqrs-mediator/mediatr",component:d("/docs/next/cqrs-mediator/mediatr","6d6"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/cqrs-mediator/queries-handlers",component:d("/docs/next/cqrs-mediator/queries-handlers","4a0"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/cqrs-mediator/wolverine",component:d("/docs/next/cqrs-mediator/wolverine","131"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/domain-driven-design/auditing",component:d("/docs/next/domain-driven-design/auditing","c52"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/domain-driven-design/domain-events",component:d("/docs/next/domain-driven-design/domain-events","bd4"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/domain-driven-design/entities-aggregates",component:d("/docs/next/domain-driven-design/entities-aggregates","b9f"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/domain-driven-design/soft-delete",component:d("/docs/next/domain-driven-design/soft-delete","526"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/domain-driven-design/value-objects",component:d("/docs/next/domain-driven-design/value-objects","9f9"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/email/overview",component:d("/docs/next/email/overview","4ba"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/email/sendgrid",component:d("/docs/next/email/sendgrid","25e"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/event-handling/distributed",component:d("/docs/next/event-handling/distributed","30d"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/event-handling/in-memory",component:d("/docs/next/event-handling/in-memory","c7a"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/event-handling/masstransit",component:d("/docs/next/event-handling/masstransit","bd2"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/event-handling/mediatr",component:d("/docs/next/event-handling/mediatr","ebf"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/event-handling/overview",component:d("/docs/next/event-handling/overview","4d3"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/event-handling/transactional-outbox",component:d("/docs/next/event-handling/transactional-outbox","232"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/event-handling/wolverine",component:d("/docs/next/event-handling/wolverine","f43"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/examples-recipes/caching",component:d("/docs/next/examples-recipes/caching","a2e"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/examples-recipes/event-handling",component:d("/docs/next/examples-recipes/event-handling","aaa"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/examples-recipes/hr-leave-management",component:d("/docs/next/examples-recipes/hr-leave-management","e54"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/examples-recipes/messaging",component:d("/docs/next/examples-recipes/messaging","27f"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/getting-started/configuration",component:d("/docs/next/getting-started/configuration","890"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/getting-started/dependency-injection",component:d("/docs/next/getting-started/dependency-injection","495"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/getting-started/installation",component:d("/docs/next/getting-started/installation","b7a"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/getting-started/overview",component:d("/docs/next/getting-started/overview","14c"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/getting-started/quick-start",component:d("/docs/next/getting-started/quick-start","c8c"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/messaging/masstransit",component:d("/docs/next/messaging/masstransit","149"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/messaging/overview",component:d("/docs/next/messaging/overview","e5d"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/messaging/state-machines",component:d("/docs/next/messaging/state-machines","15c"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/messaging/transactional-outbox",component:d("/docs/next/messaging/transactional-outbox","1e6"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/messaging/wolverine",component:d("/docs/next/messaging/wolverine","78a"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/multi-tenancy/finbuckle",component:d("/docs/next/multi-tenancy/finbuckle","f1f"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/multi-tenancy/overview",component:d("/docs/next/multi-tenancy/overview","2b4"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/persistence/caching-memory",component:d("/docs/next/persistence/caching-memory","51c"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/persistence/caching-redis",component:d("/docs/next/persistence/caching-redis","1ea"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/persistence/dapper",component:d("/docs/next/persistence/dapper","d63"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/persistence/efcore",component:d("/docs/next/persistence/efcore","744"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/persistence/linq2db",component:d("/docs/next/persistence/linq2db","642"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/persistence/repository-pattern",component:d("/docs/next/persistence/repository-pattern","fae"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/persistence/sagas",component:d("/docs/next/persistence/sagas","ec2"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/persistence/specifications",component:d("/docs/next/persistence/specifications","92b"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/persistence/unit-of-work",component:d("/docs/next/persistence/unit-of-work","6e0"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/security-web/authorization",component:d("/docs/next/security-web/authorization","7ca"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/security-web/web-utilities",component:d("/docs/next/security-web/web-utilities","3d7"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/serialization/newtonsoft",component:d("/docs/next/serialization/newtonsoft","b66"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/serialization/overview",component:d("/docs/next/serialization/overview","820"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/serialization/system-text-json",component:d("/docs/next/serialization/system-text-json","16a"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/state-machines/overview",component:d("/docs/next/state-machines/overview","6b6"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/state-machines/stateless",component:d("/docs/next/state-machines/stateless","34e"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/testing/overview",component:d("/docs/next/testing/overview","75c"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/testing/test-base-classes",component:d("/docs/next/testing/test-base-classes","947"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/next/validation/fluent-validation",component:d("/docs/next/validation/fluent-validation","195"),exact:!0,sidebar:"docsSidebar"}]}]},{path:"/docs",component:d("/docs","06f"),routes:[{path:"/docs",component:d("/docs","b2c"),routes:[{path:"/docs",component:d("/docs","71c"),exact:!0},{path:"/docs/api-reference/changelog",component:d("/docs/api-reference/changelog","f90"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/api-reference/migration-guide",component:d("/docs/api-reference/migration-guide","e22"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/api-reference/nuget-packages",component:d("/docs/api-reference/nuget-packages","922"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/architecture-guides/clean-architecture",component:d("/docs/architecture-guides/clean-architecture","393"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/architecture-guides/event-driven",component:d("/docs/architecture-guides/event-driven","4d7"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/architecture-guides/microservices",component:d("/docs/architecture-guides/microservices","f82"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/blob-storage/azure",component:d("/docs/blob-storage/azure","371"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/blob-storage/overview",component:d("/docs/blob-storage/overview","c52"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/blob-storage/s3",component:d("/docs/blob-storage/s3","2a4"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/caching/memory",component:d("/docs/caching/memory","04d"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/caching/overview",component:d("/docs/caching/overview","06f"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/caching/redis",component:d("/docs/caching/redis","f95"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/api-reference",component:d("/docs/category/api-reference","789"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/architecture-guides",component:d("/docs/category/architecture-guides","aa9"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/blob-storage",component:d("/docs/category/blob-storage","7bc"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/caching",component:d("/docs/category/caching","fc8"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/core-concepts",component:d("/docs/category/core-concepts","270"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/cqrs--mediator",component:d("/docs/category/cqrs--mediator","7b5"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/domain-driven-design",component:d("/docs/category/domain-driven-design","553"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/email",component:d("/docs/category/email","497"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/event-handling",component:d("/docs/category/event-handling","beb"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/examples--recipes",component:d("/docs/category/examples--recipes","246"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/getting-started",component:d("/docs/category/getting-started","d48"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/messaging",component:d("/docs/category/messaging","d87"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/multi-tenancy",component:d("/docs/category/multi-tenancy","4aa"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/persistence",component:d("/docs/category/persistence","d6c"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/security--web",component:d("/docs/category/security--web","392"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/serialization",component:d("/docs/category/serialization","baf"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/state-machines",component:d("/docs/category/state-machines","4e4"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/testing",component:d("/docs/category/testing","528"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/category/validation",component:d("/docs/category/validation","a2e"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/core-concepts/execution-results",component:d("/docs/core-concepts/execution-results","762"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/core-concepts/fluent-configuration",component:d("/docs/core-concepts/fluent-configuration","c31"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/core-concepts/guards",component:d("/docs/core-concepts/guards","75e"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/core-concepts/guid-generation",component:d("/docs/core-concepts/guid-generation","1f1"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/core-concepts/system-time",component:d("/docs/core-concepts/system-time","4cf"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/cqrs-mediator/command-query-bus",component:d("/docs/cqrs-mediator/command-query-bus","bc1"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/cqrs-mediator/commands-handlers",component:d("/docs/cqrs-mediator/commands-handlers","083"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/cqrs-mediator/mediatr",component:d("/docs/cqrs-mediator/mediatr","e5f"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/cqrs-mediator/queries-handlers",component:d("/docs/cqrs-mediator/queries-handlers","af8"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/cqrs-mediator/wolverine",component:d("/docs/cqrs-mediator/wolverine","cad"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/domain-driven-design/auditing",component:d("/docs/domain-driven-design/auditing","07c"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/domain-driven-design/domain-events",component:d("/docs/domain-driven-design/domain-events","083"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/domain-driven-design/entities-aggregates",component:d("/docs/domain-driven-design/entities-aggregates","1b5"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/domain-driven-design/soft-delete",component:d("/docs/domain-driven-design/soft-delete","3d7"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/domain-driven-design/value-objects",component:d("/docs/domain-driven-design/value-objects","2a9"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/email/overview",component:d("/docs/email/overview","1da"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/email/sendgrid",component:d("/docs/email/sendgrid","c9e"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/event-handling/distributed",component:d("/docs/event-handling/distributed","077"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/event-handling/in-memory",component:d("/docs/event-handling/in-memory","2d2"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/event-handling/masstransit",component:d("/docs/event-handling/masstransit","f04"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/event-handling/mediatr",component:d("/docs/event-handling/mediatr","ff9"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/event-handling/overview",component:d("/docs/event-handling/overview","79d"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/event-handling/transactional-outbox",component:d("/docs/event-handling/transactional-outbox","dde"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/event-handling/wolverine",component:d("/docs/event-handling/wolverine","94e"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/examples-recipes/caching",component:d("/docs/examples-recipes/caching","ea3"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/examples-recipes/event-handling",component:d("/docs/examples-recipes/event-handling","5b6"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/examples-recipes/hr-leave-management",component:d("/docs/examples-recipes/hr-leave-management","3bd"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/examples-recipes/messaging",component:d("/docs/examples-recipes/messaging","63c"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/getting-started/configuration",component:d("/docs/getting-started/configuration","91a"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/getting-started/dependency-injection",component:d("/docs/getting-started/dependency-injection","e67"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/getting-started/installation",component:d("/docs/getting-started/installation","c31"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/getting-started/overview",component:d("/docs/getting-started/overview","4a5"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/getting-started/quick-start",component:d("/docs/getting-started/quick-start","099"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/messaging/masstransit",component:d("/docs/messaging/masstransit","39e"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/messaging/overview",component:d("/docs/messaging/overview","a4d"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/messaging/state-machines",component:d("/docs/messaging/state-machines","7fb"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/messaging/transactional-outbox",component:d("/docs/messaging/transactional-outbox","4ed"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/messaging/wolverine",component:d("/docs/messaging/wolverine","02a"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/multi-tenancy/finbuckle",component:d("/docs/multi-tenancy/finbuckle","00b"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/multi-tenancy/overview",component:d("/docs/multi-tenancy/overview","f0d"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/persistence/caching-memory",component:d("/docs/persistence/caching-memory","50a"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/persistence/caching-redis",component:d("/docs/persistence/caching-redis","3fb"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/persistence/dapper",component:d("/docs/persistence/dapper","502"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/persistence/efcore",component:d("/docs/persistence/efcore","24d"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/persistence/linq2db",component:d("/docs/persistence/linq2db","70e"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/persistence/repository-pattern",component:d("/docs/persistence/repository-pattern","47e"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/persistence/sagas",component:d("/docs/persistence/sagas","194"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/persistence/specifications",component:d("/docs/persistence/specifications","e4c"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/persistence/unit-of-work",component:d("/docs/persistence/unit-of-work","11a"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/security-web/authorization",component:d("/docs/security-web/authorization","8d4"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/security-web/web-utilities",component:d("/docs/security-web/web-utilities","ea2"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/serialization/newtonsoft",component:d("/docs/serialization/newtonsoft","8ec"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/serialization/overview",component:d("/docs/serialization/overview","740"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/serialization/system-text-json",component:d("/docs/serialization/system-text-json","9c2"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/state-machines/overview",component:d("/docs/state-machines/overview","f5e"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/state-machines/stateless",component:d("/docs/state-machines/stateless","1cc"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/testing/overview",component:d("/docs/testing/overview","522"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/testing/test-base-classes",component:d("/docs/testing/test-base-classes","1cd"),exact:!0,sidebar:"docsSidebar"},{path:"/docs/validation/fluent-validation",component:d("/docs/validation/fluent-validation","1bf"),exact:!0,sidebar:"docsSidebar"}]}]}]},{path:"/",component:d("/","e5f"),exact:!0},{path:"*",component:d("*")}]},99886(e,t,n){"use strict";n.d(t,{o:()=>o,x:()=>i});var r=n(30758),a=n(86070);const o=r.createContext(!1);function i({children:e}){const[t,n]=(0,r.useState)(!1);return(0,r.useEffect)(()=>{n(!0)},[]),(0,a.jsx)(o.Provider,{value:t,children:e})}},86029(e,t,n){"use strict";var r=n(30758),a=n(99576),o=n(73790),i=n(41742),s=n(4784),c=n(11662);const l=[n(40505),n(93521),n(55273),n(94155),n(20877)];var u=n(81345),d=n(25557),p=n(45757),f=n(86070);function g({children:e}){return(0,f.jsx)(f.Fragment,{children:e})}var h=n(33959),m=n(90525),b=n(72646),y=n(91878),v=n(87712),x=n(99555),w=n(23555),S=n(3703),k=n(65103),_=n(89500);function E(){const{i18n:{currentLocale:e,defaultLocale:t,localeConfigs:n}}=(0,m.A)(),r=(0,x.o)(),a=n[e].htmlLang,o=e=>e.replace("-","_");return(0,f.jsxs)(h.A,{children:[Object.entries(n).map(([e,{htmlLang:t}])=>(0,f.jsx)("link",{rel:"alternate",href:r.createUrl({locale:e,fullyQualified:!0}),hrefLang:t},e)),(0,f.jsx)("link",{rel:"alternate",href:r.createUrl({locale:t,fullyQualified:!0}),hrefLang:"x-default"}),(0,f.jsx)("meta",{property:"og:locale",content:o(a)}),Object.values(n).filter(e=>a!==e.htmlLang).map(e=>(0,f.jsx)("meta",{property:"og:locale:alternate",content:o(e.htmlLang)},`meta-og-${e.htmlLang}`))]})}function C({permalink:e}){const{siteConfig:{url:t}}=(0,m.A)(),n=function(){const{siteConfig:{url:e,baseUrl:t,trailingSlash:n}}=(0,m.A)(),{pathname:r}=(0,d.zy)();return e+(0,k.applyTrailingSlash)((0,b.Ay)(r),{trailingSlash:n,baseUrl:t})}(),r=e?`${t}${e}`:n;return(0,f.jsxs)(h.A,{children:[(0,f.jsx)("meta",{property:"og:url",content:r}),(0,f.jsx)("link",{rel:"canonical",href:r})]})}function T(){const{i18n:{currentLocale:e}}=(0,m.A)(),{metadata:t,image:n}=(0,y.p)();return(0,f.jsxs)(f.Fragment,{children:[(0,f.jsxs)(h.A,{children:[(0,f.jsx)("meta",{name:"twitter:card",content:"summary_large_image"}),(0,f.jsx)("body",{className:w.w})]}),n&&(0,f.jsx)(v.be,{image:n}),(0,f.jsx)(C,{}),(0,f.jsx)(E,{}),(0,f.jsx)(_.A,{tag:S.Cy,locale:e}),(0,f.jsx)(h.A,{children:t.map((e,t)=>(0,f.jsx)("meta",{...e},t))})]})}const A=new Map;var P=n(99886),N=n(2376),j=n(85924);function L(e,...t){const n=l.map(n=>{const r=n.default?.[e]??n[e];return r?.(...t)});return()=>n.forEach(e=>e?.())}const O=function({children:e,location:t,previousLocation:n}){return(0,j.A)(()=>{n!==t&&(!function({location:e,previousLocation:t}){if(!t)return;const n=e.pathname===t.pathname,r=e.hash===t.hash,a=e.search===t.search;if(n&&r&&!a)return;const{hash:o}=e;if(o){const e=decodeURIComponent(o.substring(1)),t=document.getElementById(e);t?.scrollIntoView()}else window.scrollTo(0,0)}({location:t,previousLocation:n}),L("onRouteDidUpdate",{previousLocation:n,location:t}))},[n,t]),e};function R(e){const t=Array.from(new Set([e,decodeURI(e)])).map(e=>(0,p.u)(u.A,e)).flat();return Promise.all(t.map(e=>e.route.component.preload?.()))}class I extends r.Component{previousLocation;routeUpdateCleanupCb;constructor(e){super(e),this.previousLocation=null,this.routeUpdateCleanupCb=c.A.canUseDOM?L("onRouteUpdate",{previousLocation:null,location:this.props.location}):()=>{},this.state={nextRouteHasLoaded:!0}}shouldComponentUpdate(e,t){if(e.location===this.props.location)return t.nextRouteHasLoaded;const n=e.location;return this.previousLocation=this.props.location,this.setState({nextRouteHasLoaded:!1}),this.routeUpdateCleanupCb=L("onRouteUpdate",{previousLocation:this.previousLocation,location:n}),R(n.pathname).then(()=>{this.routeUpdateCleanupCb(),this.setState({nextRouteHasLoaded:!0})}).catch(e=>{console.warn(e),window.location.reload()}),!1}render(){const{children:e,location:t}=this.props;return(0,f.jsx)(O,{previousLocation:this.previousLocation,location:t,children:(0,f.jsx)(d.qh,{location:t,render:()=>e})})}}const D=I,F="__docusaurus-base-url-issue-banner-suggestion-container";function M(e){return`\ndocument.addEventListener('DOMContentLoaded', function maybeInsertBanner() {\n var shouldInsert = typeof window['docusaurus'] === 'undefined';\n shouldInsert && insertBanner();\n});\n\nfunction insertBanner() {\n var bannerContainer = document.createElement('div');\n bannerContainer.id = '__docusaurus-base-url-issue-banner-container';\n var bannerHtml = ${JSON.stringify(function(e){return`\n
\n

Your Docusaurus site did not load properly.

\n

A very common reason is a wrong site baseUrl configuration.

\n

Current configured baseUrl = ${e} ${"/"===e?" (default value)":""}

\n

We suggest trying baseUrl =

\n
\n`}(e)).replace(/!0===e.exact))return A.set(e.pathname,e.pathname),e;const t=e.pathname.trim().replace(/(?:\/index)?\.html$/,"")||"/";return A.set(e.pathname,t),{...e,pathname:t}}((0,d.zy)());return(0,f.jsx)(D,{location:e,children:Q})}function W(){return(0,f.jsx)(U.A,{children:(0,f.jsx)(N.l,{children:(0,f.jsxs)(P.x,{children:[(0,f.jsxs)(g,{children:[(0,f.jsx)(B,{}),(0,f.jsx)(T,{}),(0,f.jsx)($,{}),(0,f.jsx)(V,{})]}),(0,f.jsx)(H,{})]})})})}var G=n(84054);const Y=function(e){try{return document.createElement("link").relList.supports(e)}catch{return!1}}("prefetch")?function(e){return new Promise((t,n)=>{if("undefined"==typeof document)return void n();const r=document.createElement("link");r.setAttribute("rel","prefetch"),r.setAttribute("href",e),r.onload=()=>t(),r.onerror=()=>n();const a=document.getElementsByTagName("head")[0]??document.getElementsByName("script")[0]?.parentNode;a?.appendChild(r)})}:function(e){return new Promise((t,n)=>{const r=new XMLHttpRequest;r.open("GET",e,!0),r.withCredentials=!0,r.onload=()=>{200===r.status?t():n()},r.send(null)})};var K=n(73946);const Z=new Set,X=new Set,J=()=>navigator.connection?.effectiveType.includes("2g")||navigator.connection?.saveData,ee={prefetch:e=>{if(!(e=>!J()&&!X.has(e)&&!Z.has(e))(e))return!1;Z.add(e);const t=(0,p.u)(u.A,e).flatMap(e=>{return t=e.route.path,Object.entries(G).filter(([e])=>e.replace(/-[^-]+$/,"")===t).flatMap(([,e])=>Object.values((0,K.A)(e)));var t});return Promise.all(t.map(e=>{const t=n.gca(e);return t&&!t.includes("undefined")?Y(t).catch(()=>{}):Promise.resolve()}))},preload:e=>!!(e=>!J()&&!X.has(e))(e)&&(X.add(e),R(e))},te=Object.freeze(ee);function ne({children:e}){return"hash"===s.default.future.experimental_router?(0,f.jsx)(i.I9,{children:e}):(0,f.jsx)(i.Kd,{children:e})}const re=Boolean(!0);if(c.A.canUseDOM){window.docusaurus=te;const e=document.getElementById("__docusaurus"),t=(0,f.jsx)(o.vd,{children:(0,f.jsx)(ne,{children:(0,f.jsx)(W,{})})}),n=(e,t)=>{console.error("Docusaurus React Root onRecoverableError:",e,t)},i=()=>{if(window.docusaurusRoot)window.docusaurusRoot.render(t);else if(re)window.docusaurusRoot=a.hydrateRoot(e,t,{onRecoverableError:n});else{const r=a.createRoot(e,{onRecoverableError:n});r.render(t),window.docusaurusRoot=r}};R(window.location.pathname).then(()=>{(0,r.startTransition)(i)})}},2376(e,t,n){"use strict";n.d(t,{o:()=>d,l:()=>p});var r=n(30758),a=n(4784);const o=JSON.parse('{"docusaurus-plugin-content-docs":{"default":{"path":"/docs","versions":[{"name":"current","label":"Next","isLast":false,"path":"/docs/next","mainDocId":"index","docs":[{"id":"api-reference/changelog","path":"/docs/next/api-reference/changelog","sidebar":"docsSidebar"},{"id":"api-reference/migration-guide","path":"/docs/next/api-reference/migration-guide","sidebar":"docsSidebar"},{"id":"api-reference/nuget-packages","path":"/docs/next/api-reference/nuget-packages","sidebar":"docsSidebar"},{"id":"architecture-guides/clean-architecture","path":"/docs/next/architecture-guides/clean-architecture","sidebar":"docsSidebar"},{"id":"architecture-guides/event-driven","path":"/docs/next/architecture-guides/event-driven","sidebar":"docsSidebar"},{"id":"architecture-guides/microservices","path":"/docs/next/architecture-guides/microservices","sidebar":"docsSidebar"},{"id":"blob-storage/azure","path":"/docs/next/blob-storage/azure","sidebar":"docsSidebar"},{"id":"blob-storage/overview","path":"/docs/next/blob-storage/overview","sidebar":"docsSidebar"},{"id":"blob-storage/s3","path":"/docs/next/blob-storage/s3","sidebar":"docsSidebar"},{"id":"caching/memory","path":"/docs/next/caching/memory","sidebar":"docsSidebar"},{"id":"caching/overview","path":"/docs/next/caching/overview","sidebar":"docsSidebar"},{"id":"caching/redis","path":"/docs/next/caching/redis","sidebar":"docsSidebar"},{"id":"core-concepts/execution-results","path":"/docs/next/core-concepts/execution-results","sidebar":"docsSidebar"},{"id":"core-concepts/fluent-configuration","path":"/docs/next/core-concepts/fluent-configuration","sidebar":"docsSidebar"},{"id":"core-concepts/guards","path":"/docs/next/core-concepts/guards","sidebar":"docsSidebar"},{"id":"core-concepts/guid-generation","path":"/docs/next/core-concepts/guid-generation","sidebar":"docsSidebar"},{"id":"core-concepts/modular-composition","path":"/docs/next/core-concepts/modular-composition","sidebar":"docsSidebar"},{"id":"core-concepts/system-time","path":"/docs/next/core-concepts/system-time","sidebar":"docsSidebar"},{"id":"cqrs-mediator/command-query-bus","path":"/docs/next/cqrs-mediator/command-query-bus","sidebar":"docsSidebar"},{"id":"cqrs-mediator/commands-handlers","path":"/docs/next/cqrs-mediator/commands-handlers","sidebar":"docsSidebar"},{"id":"cqrs-mediator/mediatr","path":"/docs/next/cqrs-mediator/mediatr","sidebar":"docsSidebar"},{"id":"cqrs-mediator/queries-handlers","path":"/docs/next/cqrs-mediator/queries-handlers","sidebar":"docsSidebar"},{"id":"cqrs-mediator/wolverine","path":"/docs/next/cqrs-mediator/wolverine","sidebar":"docsSidebar"},{"id":"domain-driven-design/auditing","path":"/docs/next/domain-driven-design/auditing","sidebar":"docsSidebar"},{"id":"domain-driven-design/domain-events","path":"/docs/next/domain-driven-design/domain-events","sidebar":"docsSidebar"},{"id":"domain-driven-design/entities-aggregates","path":"/docs/next/domain-driven-design/entities-aggregates","sidebar":"docsSidebar"},{"id":"domain-driven-design/soft-delete","path":"/docs/next/domain-driven-design/soft-delete","sidebar":"docsSidebar"},{"id":"domain-driven-design/value-objects","path":"/docs/next/domain-driven-design/value-objects","sidebar":"docsSidebar"},{"id":"email/overview","path":"/docs/next/email/overview","sidebar":"docsSidebar"},{"id":"email/sendgrid","path":"/docs/next/email/sendgrid","sidebar":"docsSidebar"},{"id":"event-handling/distributed","path":"/docs/next/event-handling/distributed","sidebar":"docsSidebar"},{"id":"event-handling/in-memory","path":"/docs/next/event-handling/in-memory","sidebar":"docsSidebar"},{"id":"event-handling/masstransit","path":"/docs/next/event-handling/masstransit","sidebar":"docsSidebar"},{"id":"event-handling/mediatr","path":"/docs/next/event-handling/mediatr","sidebar":"docsSidebar"},{"id":"event-handling/overview","path":"/docs/next/event-handling/overview","sidebar":"docsSidebar"},{"id":"event-handling/transactional-outbox","path":"/docs/next/event-handling/transactional-outbox","sidebar":"docsSidebar"},{"id":"event-handling/wolverine","path":"/docs/next/event-handling/wolverine","sidebar":"docsSidebar"},{"id":"examples-recipes/caching","path":"/docs/next/examples-recipes/caching","sidebar":"docsSidebar"},{"id":"examples-recipes/event-handling","path":"/docs/next/examples-recipes/event-handling","sidebar":"docsSidebar"},{"id":"examples-recipes/hr-leave-management","path":"/docs/next/examples-recipes/hr-leave-management","sidebar":"docsSidebar"},{"id":"examples-recipes/messaging","path":"/docs/next/examples-recipes/messaging","sidebar":"docsSidebar"},{"id":"getting-started/configuration","path":"/docs/next/getting-started/configuration","sidebar":"docsSidebar"},{"id":"getting-started/dependency-injection","path":"/docs/next/getting-started/dependency-injection","sidebar":"docsSidebar"},{"id":"getting-started/installation","path":"/docs/next/getting-started/installation","sidebar":"docsSidebar"},{"id":"getting-started/overview","path":"/docs/next/getting-started/overview","sidebar":"docsSidebar"},{"id":"getting-started/quick-start","path":"/docs/next/getting-started/quick-start","sidebar":"docsSidebar"},{"id":"index","path":"/docs/next/"},{"id":"messaging/masstransit","path":"/docs/next/messaging/masstransit","sidebar":"docsSidebar"},{"id":"messaging/overview","path":"/docs/next/messaging/overview","sidebar":"docsSidebar"},{"id":"messaging/state-machines","path":"/docs/next/messaging/state-machines","sidebar":"docsSidebar"},{"id":"messaging/transactional-outbox","path":"/docs/next/messaging/transactional-outbox","sidebar":"docsSidebar"},{"id":"messaging/wolverine","path":"/docs/next/messaging/wolverine","sidebar":"docsSidebar"},{"id":"multi-tenancy/finbuckle","path":"/docs/next/multi-tenancy/finbuckle","sidebar":"docsSidebar"},{"id":"multi-tenancy/overview","path":"/docs/next/multi-tenancy/overview","sidebar":"docsSidebar"},{"id":"persistence/caching-memory","path":"/docs/next/persistence/caching-memory","sidebar":"docsSidebar"},{"id":"persistence/caching-redis","path":"/docs/next/persistence/caching-redis","sidebar":"docsSidebar"},{"id":"persistence/dapper","path":"/docs/next/persistence/dapper","sidebar":"docsSidebar"},{"id":"persistence/efcore","path":"/docs/next/persistence/efcore","sidebar":"docsSidebar"},{"id":"persistence/linq2db","path":"/docs/next/persistence/linq2db","sidebar":"docsSidebar"},{"id":"persistence/repository-pattern","path":"/docs/next/persistence/repository-pattern","sidebar":"docsSidebar"},{"id":"persistence/sagas","path":"/docs/next/persistence/sagas","sidebar":"docsSidebar"},{"id":"persistence/specifications","path":"/docs/next/persistence/specifications","sidebar":"docsSidebar"},{"id":"persistence/unit-of-work","path":"/docs/next/persistence/unit-of-work","sidebar":"docsSidebar"},{"id":"security-web/authorization","path":"/docs/next/security-web/authorization","sidebar":"docsSidebar"},{"id":"security-web/web-utilities","path":"/docs/next/security-web/web-utilities","sidebar":"docsSidebar"},{"id":"serialization/newtonsoft","path":"/docs/next/serialization/newtonsoft","sidebar":"docsSidebar"},{"id":"serialization/overview","path":"/docs/next/serialization/overview","sidebar":"docsSidebar"},{"id":"serialization/system-text-json","path":"/docs/next/serialization/system-text-json","sidebar":"docsSidebar"},{"id":"state-machines/overview","path":"/docs/next/state-machines/overview","sidebar":"docsSidebar"},{"id":"state-machines/stateless","path":"/docs/next/state-machines/stateless","sidebar":"docsSidebar"},{"id":"testing/overview","path":"/docs/next/testing/overview","sidebar":"docsSidebar"},{"id":"testing/test-base-classes","path":"/docs/next/testing/test-base-classes","sidebar":"docsSidebar"},{"id":"validation/fluent-validation","path":"/docs/next/validation/fluent-validation","sidebar":"docsSidebar"},{"id":"/category/getting-started","path":"/docs/next/category/getting-started","sidebar":"docsSidebar"},{"id":"/category/core-concepts","path":"/docs/next/category/core-concepts","sidebar":"docsSidebar"},{"id":"/category/domain-driven-design","path":"/docs/next/category/domain-driven-design","sidebar":"docsSidebar"},{"id":"/category/persistence","path":"/docs/next/category/persistence","sidebar":"docsSidebar"},{"id":"/category/cqrs--mediator","path":"/docs/next/category/cqrs--mediator","sidebar":"docsSidebar"},{"id":"/category/event-handling","path":"/docs/next/category/event-handling","sidebar":"docsSidebar"},{"id":"/category/messaging","path":"/docs/next/category/messaging","sidebar":"docsSidebar"},{"id":"/category/state-machines","path":"/docs/next/category/state-machines","sidebar":"docsSidebar"},{"id":"/category/caching","path":"/docs/next/category/caching","sidebar":"docsSidebar"},{"id":"/category/blob-storage","path":"/docs/next/category/blob-storage","sidebar":"docsSidebar"},{"id":"/category/serialization","path":"/docs/next/category/serialization","sidebar":"docsSidebar"},{"id":"/category/validation","path":"/docs/next/category/validation","sidebar":"docsSidebar"},{"id":"/category/email","path":"/docs/next/category/email","sidebar":"docsSidebar"},{"id":"/category/multi-tenancy","path":"/docs/next/category/multi-tenancy","sidebar":"docsSidebar"},{"id":"/category/security--web","path":"/docs/next/category/security--web","sidebar":"docsSidebar"},{"id":"/category/architecture-guides","path":"/docs/next/category/architecture-guides","sidebar":"docsSidebar"},{"id":"/category/examples--recipes","path":"/docs/next/category/examples--recipes","sidebar":"docsSidebar"},{"id":"/category/testing","path":"/docs/next/category/testing","sidebar":"docsSidebar"},{"id":"/category/api-reference","path":"/docs/next/category/api-reference","sidebar":"docsSidebar"}],"draftIds":[],"sidebars":{"docsSidebar":{"link":{"path":"/docs/next/category/getting-started","label":"Getting Started"}}}},{"name":"2.4.1","label":"2.4.1","isLast":true,"path":"/docs","mainDocId":"index","docs":[{"id":"api-reference/changelog","path":"/docs/api-reference/changelog","sidebar":"docsSidebar"},{"id":"api-reference/migration-guide","path":"/docs/api-reference/migration-guide","sidebar":"docsSidebar"},{"id":"api-reference/nuget-packages","path":"/docs/api-reference/nuget-packages","sidebar":"docsSidebar"},{"id":"architecture-guides/clean-architecture","path":"/docs/architecture-guides/clean-architecture","sidebar":"docsSidebar"},{"id":"architecture-guides/event-driven","path":"/docs/architecture-guides/event-driven","sidebar":"docsSidebar"},{"id":"architecture-guides/microservices","path":"/docs/architecture-guides/microservices","sidebar":"docsSidebar"},{"id":"blob-storage/azure","path":"/docs/blob-storage/azure","sidebar":"docsSidebar"},{"id":"blob-storage/overview","path":"/docs/blob-storage/overview","sidebar":"docsSidebar"},{"id":"blob-storage/s3","path":"/docs/blob-storage/s3","sidebar":"docsSidebar"},{"id":"caching/memory","path":"/docs/caching/memory","sidebar":"docsSidebar"},{"id":"caching/overview","path":"/docs/caching/overview","sidebar":"docsSidebar"},{"id":"caching/redis","path":"/docs/caching/redis","sidebar":"docsSidebar"},{"id":"core-concepts/execution-results","path":"/docs/core-concepts/execution-results","sidebar":"docsSidebar"},{"id":"core-concepts/fluent-configuration","path":"/docs/core-concepts/fluent-configuration","sidebar":"docsSidebar"},{"id":"core-concepts/guards","path":"/docs/core-concepts/guards","sidebar":"docsSidebar"},{"id":"core-concepts/guid-generation","path":"/docs/core-concepts/guid-generation","sidebar":"docsSidebar"},{"id":"core-concepts/system-time","path":"/docs/core-concepts/system-time","sidebar":"docsSidebar"},{"id":"cqrs-mediator/command-query-bus","path":"/docs/cqrs-mediator/command-query-bus","sidebar":"docsSidebar"},{"id":"cqrs-mediator/commands-handlers","path":"/docs/cqrs-mediator/commands-handlers","sidebar":"docsSidebar"},{"id":"cqrs-mediator/mediatr","path":"/docs/cqrs-mediator/mediatr","sidebar":"docsSidebar"},{"id":"cqrs-mediator/queries-handlers","path":"/docs/cqrs-mediator/queries-handlers","sidebar":"docsSidebar"},{"id":"cqrs-mediator/wolverine","path":"/docs/cqrs-mediator/wolverine","sidebar":"docsSidebar"},{"id":"domain-driven-design/auditing","path":"/docs/domain-driven-design/auditing","sidebar":"docsSidebar"},{"id":"domain-driven-design/domain-events","path":"/docs/domain-driven-design/domain-events","sidebar":"docsSidebar"},{"id":"domain-driven-design/entities-aggregates","path":"/docs/domain-driven-design/entities-aggregates","sidebar":"docsSidebar"},{"id":"domain-driven-design/soft-delete","path":"/docs/domain-driven-design/soft-delete","sidebar":"docsSidebar"},{"id":"domain-driven-design/value-objects","path":"/docs/domain-driven-design/value-objects","sidebar":"docsSidebar"},{"id":"email/overview","path":"/docs/email/overview","sidebar":"docsSidebar"},{"id":"email/sendgrid","path":"/docs/email/sendgrid","sidebar":"docsSidebar"},{"id":"event-handling/distributed","path":"/docs/event-handling/distributed","sidebar":"docsSidebar"},{"id":"event-handling/in-memory","path":"/docs/event-handling/in-memory","sidebar":"docsSidebar"},{"id":"event-handling/masstransit","path":"/docs/event-handling/masstransit","sidebar":"docsSidebar"},{"id":"event-handling/mediatr","path":"/docs/event-handling/mediatr","sidebar":"docsSidebar"},{"id":"event-handling/overview","path":"/docs/event-handling/overview","sidebar":"docsSidebar"},{"id":"event-handling/transactional-outbox","path":"/docs/event-handling/transactional-outbox","sidebar":"docsSidebar"},{"id":"event-handling/wolverine","path":"/docs/event-handling/wolverine","sidebar":"docsSidebar"},{"id":"examples-recipes/caching","path":"/docs/examples-recipes/caching","sidebar":"docsSidebar"},{"id":"examples-recipes/event-handling","path":"/docs/examples-recipes/event-handling","sidebar":"docsSidebar"},{"id":"examples-recipes/hr-leave-management","path":"/docs/examples-recipes/hr-leave-management","sidebar":"docsSidebar"},{"id":"examples-recipes/messaging","path":"/docs/examples-recipes/messaging","sidebar":"docsSidebar"},{"id":"getting-started/configuration","path":"/docs/getting-started/configuration","sidebar":"docsSidebar"},{"id":"getting-started/dependency-injection","path":"/docs/getting-started/dependency-injection","sidebar":"docsSidebar"},{"id":"getting-started/installation","path":"/docs/getting-started/installation","sidebar":"docsSidebar"},{"id":"getting-started/overview","path":"/docs/getting-started/overview","sidebar":"docsSidebar"},{"id":"getting-started/quick-start","path":"/docs/getting-started/quick-start","sidebar":"docsSidebar"},{"id":"index","path":"/docs/"},{"id":"messaging/masstransit","path":"/docs/messaging/masstransit","sidebar":"docsSidebar"},{"id":"messaging/overview","path":"/docs/messaging/overview","sidebar":"docsSidebar"},{"id":"messaging/state-machines","path":"/docs/messaging/state-machines","sidebar":"docsSidebar"},{"id":"messaging/transactional-outbox","path":"/docs/messaging/transactional-outbox","sidebar":"docsSidebar"},{"id":"messaging/wolverine","path":"/docs/messaging/wolverine","sidebar":"docsSidebar"},{"id":"multi-tenancy/finbuckle","path":"/docs/multi-tenancy/finbuckle","sidebar":"docsSidebar"},{"id":"multi-tenancy/overview","path":"/docs/multi-tenancy/overview","sidebar":"docsSidebar"},{"id":"persistence/caching-memory","path":"/docs/persistence/caching-memory","sidebar":"docsSidebar"},{"id":"persistence/caching-redis","path":"/docs/persistence/caching-redis","sidebar":"docsSidebar"},{"id":"persistence/dapper","path":"/docs/persistence/dapper","sidebar":"docsSidebar"},{"id":"persistence/efcore","path":"/docs/persistence/efcore","sidebar":"docsSidebar"},{"id":"persistence/linq2db","path":"/docs/persistence/linq2db","sidebar":"docsSidebar"},{"id":"persistence/repository-pattern","path":"/docs/persistence/repository-pattern","sidebar":"docsSidebar"},{"id":"persistence/sagas","path":"/docs/persistence/sagas","sidebar":"docsSidebar"},{"id":"persistence/specifications","path":"/docs/persistence/specifications","sidebar":"docsSidebar"},{"id":"persistence/unit-of-work","path":"/docs/persistence/unit-of-work","sidebar":"docsSidebar"},{"id":"security-web/authorization","path":"/docs/security-web/authorization","sidebar":"docsSidebar"},{"id":"security-web/web-utilities","path":"/docs/security-web/web-utilities","sidebar":"docsSidebar"},{"id":"serialization/newtonsoft","path":"/docs/serialization/newtonsoft","sidebar":"docsSidebar"},{"id":"serialization/overview","path":"/docs/serialization/overview","sidebar":"docsSidebar"},{"id":"serialization/system-text-json","path":"/docs/serialization/system-text-json","sidebar":"docsSidebar"},{"id":"state-machines/overview","path":"/docs/state-machines/overview","sidebar":"docsSidebar"},{"id":"state-machines/stateless","path":"/docs/state-machines/stateless","sidebar":"docsSidebar"},{"id":"testing/overview","path":"/docs/testing/overview","sidebar":"docsSidebar"},{"id":"testing/test-base-classes","path":"/docs/testing/test-base-classes","sidebar":"docsSidebar"},{"id":"validation/fluent-validation","path":"/docs/validation/fluent-validation","sidebar":"docsSidebar"},{"id":"/category/getting-started","path":"/docs/category/getting-started","sidebar":"docsSidebar"},{"id":"/category/core-concepts","path":"/docs/category/core-concepts","sidebar":"docsSidebar"},{"id":"/category/domain-driven-design","path":"/docs/category/domain-driven-design","sidebar":"docsSidebar"},{"id":"/category/persistence","path":"/docs/category/persistence","sidebar":"docsSidebar"},{"id":"/category/cqrs--mediator","path":"/docs/category/cqrs--mediator","sidebar":"docsSidebar"},{"id":"/category/event-handling","path":"/docs/category/event-handling","sidebar":"docsSidebar"},{"id":"/category/messaging","path":"/docs/category/messaging","sidebar":"docsSidebar"},{"id":"/category/state-machines","path":"/docs/category/state-machines","sidebar":"docsSidebar"},{"id":"/category/caching","path":"/docs/category/caching","sidebar":"docsSidebar"},{"id":"/category/blob-storage","path":"/docs/category/blob-storage","sidebar":"docsSidebar"},{"id":"/category/serialization","path":"/docs/category/serialization","sidebar":"docsSidebar"},{"id":"/category/validation","path":"/docs/category/validation","sidebar":"docsSidebar"},{"id":"/category/email","path":"/docs/category/email","sidebar":"docsSidebar"},{"id":"/category/multi-tenancy","path":"/docs/category/multi-tenancy","sidebar":"docsSidebar"},{"id":"/category/security--web","path":"/docs/category/security--web","sidebar":"docsSidebar"},{"id":"/category/architecture-guides","path":"/docs/category/architecture-guides","sidebar":"docsSidebar"},{"id":"/category/examples--recipes","path":"/docs/category/examples--recipes","sidebar":"docsSidebar"},{"id":"/category/testing","path":"/docs/category/testing","sidebar":"docsSidebar"},{"id":"/category/api-reference","path":"/docs/category/api-reference","sidebar":"docsSidebar"}],"draftIds":[],"sidebars":{"docsSidebar":{"link":{"path":"/docs/category/getting-started","label":"Getting Started"}}}}],"breadcrumbs":true}},"docusaurus-plugin-google-gtag":{"default":{"trackingID":["G-LXEZV2X7W3"],"anonymizeIP":true,"id":"default"}}}'),i=JSON.parse('{"defaultLocale":"en","locales":["en"],"path":"i18n","currentLocale":"en","localeConfigs":{"en":{"label":"English","direction":"ltr","htmlLang":"en","calendar":"gregory","path":"en"}}}');var s=n(22654);const c=JSON.parse('{"docusaurusVersion":"3.4.0","siteVersion":"0.0.0","pluginVersions":{"docusaurus-plugin-content-docs":{"type":"package","name":"@docusaurus/plugin-content-docs","version":"3.4.0"},"docusaurus-plugin-content-pages":{"type":"package","name":"@docusaurus/plugin-content-pages","version":"3.4.0"},"docusaurus-plugin-google-gtag":{"type":"package","name":"@docusaurus/plugin-google-gtag","version":"3.4.0"},"docusaurus-plugin-sitemap":{"type":"package","name":"@docusaurus/plugin-sitemap","version":"3.4.0"},"docusaurus-theme-classic":{"type":"package","name":"@docusaurus/theme-classic","version":"3.4.0"},"tailwind-plugin":{"type":"local"},"seo-structured-data":{"type":"local"},"docusaurus-theme-mermaid":{"type":"package","name":"@docusaurus/theme-mermaid","version":"3.4.0"},"@easyops-cn/docusaurus-search-local":{"type":"package","name":"@easyops-cn/docusaurus-search-local","version":"0.44.6"}}}');var l=n(86070);const u={siteConfig:a.default,siteMetadata:c,globalData:o,i18n:i,codeTranslations:s},d=r.createContext(u);function p({children:e}){return(0,l.jsx)(d.Provider,{value:u,children:e})}},88233(e,t,n){"use strict";n.d(t,{A:()=>h});var r=n(30758),a=n(11662),o=n(33959),i=n(65103),s=n(44820),c=n(65777),l=n(86070);function u({error:e,tryAgain:t}){return(0,l.jsxs)("div",{style:{display:"flex",flexDirection:"column",justifyContent:"center",alignItems:"flex-start",minHeight:"100vh",width:"100%",maxWidth:"80ch",fontSize:"20px",margin:"0 auto",padding:"1rem"},children:[(0,l.jsx)("h1",{style:{fontSize:"3rem"},children:"This page crashed"}),(0,l.jsx)("button",{type:"button",onClick:t,style:{margin:"1rem 0",fontSize:"2rem",cursor:"pointer",borderRadius:20,padding:"1rem"},children:"Try again"}),(0,l.jsx)(d,{error:e})]})}function d({error:e}){const t=(0,i.getErrorCausalChain)(e).map(e=>e.message).join("\n\nCause:\n");return(0,l.jsx)("p",{style:{whiteSpace:"pre-wrap"},children:t})}function p({children:e}){return(0,l.jsx)(c.W,{value:{plugin:{name:"docusaurus-core-error-boundary",id:"default"}},children:e})}function f({error:e,tryAgain:t}){return(0,l.jsx)(p,{children:(0,l.jsxs)(h,{fallback:()=>(0,l.jsx)(u,{error:e,tryAgain:t}),children:[(0,l.jsx)(o.A,{children:(0,l.jsx)("title",{children:"Page Error"})}),(0,l.jsx)(s.A,{children:(0,l.jsx)(u,{error:e,tryAgain:t})})]})})}const g=e=>(0,l.jsx)(f,{...e});class h extends r.Component{constructor(e){super(e),this.state={error:null}}componentDidCatch(e){a.A.canUseDOM&&this.setState({error:e})}render(){const{children:e}=this.props,{error:t}=this.state;if(t){const e={error:t,tryAgain:()=>this.setState({error:null})};return(this.props.fallback??g)(e)}return e??null}}},11662(e,t,n){"use strict";n.d(t,{A:()=>a});const r="undefined"!=typeof window&&"document"in window&&"createElement"in window.document,a={canUseDOM:r,canUseEventListeners:r&&("addEventListener"in window||"attachEvent"in window),canUseIntersectionObserver:r&&"IntersectionObserver"in window,canUseViewport:r&&"screen"in window}},33959(e,t,n){"use strict";n.d(t,{A:()=>o});n(30758);var r=n(73790),a=n(86070);function o(e){return(0,a.jsx)(r.mg,{...e})}},46753(e,t,n){"use strict";n.d(t,{A:()=>f});var r=n(30758),a=n(41742),o=n(65103),i=n(90525),s=n(6887),c=n(11662),l=n(86820),u=n(72646),d=n(86070);function p({isNavLink:e,to:t,href:n,activeClassName:p,isActive:f,"data-noBrokenLinkCheck":g,autoAddBaseUrl:h=!0,...m},b){const{siteConfig:y}=(0,i.A)(),{trailingSlash:v,baseUrl:x}=y,w=y.future.experimental_router,{withBaseUrl:S}=(0,u.hH)(),k=(0,l.A)(),_=(0,r.useRef)(null);(0,r.useImperativeHandle)(b,()=>_.current);const E=t||n;const C=(0,s.A)(E),T=E?.replace("pathname://","");let A=void 0!==T?(P=T,h&&(e=>e.startsWith("/"))(P)?S(P):P):void 0;var P;"hash"===w&&A?.startsWith("./")&&(A=A?.slice(1)),A&&C&&(A=(0,o.applyTrailingSlash)(A,{trailingSlash:v,baseUrl:x}));const N=(0,r.useRef)(!1),j=e?a.k2:a.N_,L=c.A.canUseIntersectionObserver,O=(0,r.useRef)(),R=()=>{N.current||null==A||(window.docusaurus.preload(A),N.current=!0)};(0,r.useEffect)(()=>(!L&&C&&null!=A&&window.docusaurus.prefetch(A),()=>{L&&O.current&&O.current.disconnect()}),[O,A,L,C]);const I=A?.startsWith("#")??!1,D=!m.target||"_self"===m.target,F=!A||!C||!D;return g||!I&&F||k.collectLink(A),m.id&&k.collectAnchor(m.id),F?(0,d.jsx)("a",{ref:_,href:A,...E&&!C&&{target:"_blank",rel:"noopener noreferrer"},...m}):(0,d.jsx)(j,{...m,onMouseEnter:R,onTouchStart:R,innerRef:e=>{_.current=e,L&&e&&C&&(O.current=new window.IntersectionObserver(t=>{t.forEach(t=>{e===t.target&&(t.isIntersecting||t.intersectionRatio>0)&&(O.current.unobserve(e),O.current.disconnect(),null!=A&&window.docusaurus.prefetch(A))})}),O.current.observe(e))},to:A,...e&&{isActive:f,activeClassName:p}})}const f=r.forwardRef(p)},61288(e,t,n){"use strict";n.d(t,{A:()=>l,T:()=>c});var r=n(30758),a=n(86070);function o(e,t){const n=e.split(/(\{\w+\})/).map((e,n)=>{if(n%2==1){const n=t?.[e.slice(1,-1)];if(void 0!==n)return n}return e});return n.some(e=>(0,r.isValidElement)(e))?n.map((e,t)=>(0,r.isValidElement)(e)?r.cloneElement(e,{key:t}):e).filter(e=>""!==e):n.join("")}var i=n(22654);function s({id:e,message:t}){if(void 0===e&&void 0===t)throw new Error("Docusaurus translation declarations must have at least a translation id or a default translation message");return i[e??t]??t??e}function c({message:e,id:t},n){return o(s({message:e,id:t}),n)}function l({children:e,id:t,values:n}){if(e&&"string"!=typeof e)throw console.warn("Illegal children",e),new Error("The Docusaurus component only accept simple string values");const r=s({message:e,id:t});return(0,a.jsx)(a.Fragment,{children:o(r,n)})}},91260(e,t,n){"use strict";n.d(t,{W:()=>r});const r="default"},6887(e,t,n){"use strict";function r(e){return/^(?:\w*:|\/\/)/.test(e)}function a(e){return void 0!==e&&!r(e)}n.d(t,{A:()=>a,z:()=>r})},72646(e,t,n){"use strict";n.d(t,{Ay:()=>s,hH:()=>i});var r=n(30758),a=n(90525),o=n(6887);function i(){const{siteConfig:e}=(0,a.A)(),{baseUrl:t,url:n}=e,i=e.future.experimental_router,s=(0,r.useCallback)((e,r)=>function({siteUrl:e,baseUrl:t,url:n,options:{forcePrependBaseUrl:r=!1,absolute:a=!1}={},router:i}){if(!n||n.startsWith("#")||(0,o.z)(n))return n;if("hash"===i)return n.startsWith("/")?`.${n}`:`./${n}`;if(r)return t+n.replace(/^\//,"");if(n===t.replace(/\/$/,""))return t;const s=n.startsWith(t)?n:t+n.replace(/^\//,"");return a?e+s:s}({siteUrl:n,baseUrl:t,url:e,options:r,router:i}),[n,t,i]);return{withBaseUrl:s}}function s(e,t={}){const{withBaseUrl:n}=i();return n(e,t)}},86820(e,t,n){"use strict";n.d(t,{A:()=>o});var r=n(30758);n(86070);const a=r.createContext({collectAnchor:()=>{},collectLink:()=>{}});function o(){return(0,r.useContext)(a)}},90525(e,t,n){"use strict";n.d(t,{A:()=>o});var r=n(30758),a=n(2376);function o(){return(0,r.useContext)(a.o)}},81264(e,t,n){"use strict";n.d(t,{A:()=>o});var r=n(30758),a=n(99886);function o(){return(0,r.useContext)(a.o)}},85924(e,t,n){"use strict";n.d(t,{A:()=>a});var r=n(30758);const a=n(11662).A.canUseDOM?r.useLayoutEffect:r.useEffect},73946(e,t,n){"use strict";n.d(t,{A:()=>r});function r(e){const t={};return function e(n,r){Object.entries(n).forEach(([n,a])=>{const o=r?`${r}.${n}`:n;var i;"object"==typeof(i=a)&&i&&Object.keys(i).length>0?e(a,o):t[o]=a})}(e),t}},65777(e,t,n){"use strict";n.d(t,{W:()=>i,o:()=>o});var r=n(30758),a=n(86070);const o=r.createContext(null);function i({children:e,value:t}){const n=r.useContext(o),i=(0,r.useMemo)(()=>function({parent:e,value:t}){if(!e){if(!t)throw new Error("Unexpected: no Docusaurus route context found");if(!("plugin"in t))throw new Error("Unexpected: Docusaurus topmost route context has no `plugin` attribute");return t}const n={...e.data,...t?.data};return{plugin:e.plugin,data:n}}({parent:n,value:t}),[n,t]);return(0,a.jsx)(o.Provider,{value:i,children:e})}},72631(e,t,n){"use strict";n.d(t,{zK:()=>h,vT:()=>p,Gy:()=>u,HW:()=>m,ht:()=>d,r7:()=>g,jh:()=>f});var r=n(25557),a=n(90525),o=n(91260);function i(e,t={}){const n=function(){const{globalData:e}=(0,a.A)();return e}()[e];if(!n&&t.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin.`);return n}const s=e=>e.versions.find(e=>e.isLast);function c(e,t){const n=function(e,t){const n=s(e);return[...e.versions.filter(e=>e!==n),n].find(e=>!!(0,r.B6)(t,{path:e.path,exact:!1,strict:!1}))}(e,t),a=n?.docs.find(e=>!!(0,r.B6)(t,{path:e.path,exact:!0,strict:!1}));return{activeVersion:n,activeDoc:a,alternateDocVersions:a?function(t){const n={};return e.versions.forEach(e=>{e.docs.forEach(r=>{r.id===t&&(n[e.name]=r)})}),n}(a.id):{}}}const l={},u=()=>i("docusaurus-plugin-content-docs")??l,d=e=>{try{return function(e,t=o.W,n={}){const r=i(e),a=r?.[t];if(!a&&n.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin with id "${t}".`);return a}("docusaurus-plugin-content-docs",e,{failfast:!0})}catch(t){throw new Error("You are using a feature of the Docusaurus docs plugin, but this plugin does not seem to be enabled"+("Default"===e?"":` (pluginId=${e}`),{cause:t})}};function p(e={}){const t=u(),{pathname:n}=(0,r.zy)();return function(e,t,n={}){const a=Object.entries(e).sort((e,t)=>t[1].path.localeCompare(e[1].path)).find(([,e])=>!!(0,r.B6)(t,{path:e.path,exact:!1,strict:!1})),o=a?{pluginId:a[0],pluginData:a[1]}:void 0;if(!o&&n.failfast)throw new Error(`Can't find active docs plugin for "${t}" pathname, while it was expected to be found. Maybe you tried to use a docs feature that can only be used on a docs-related page? Existing docs plugin paths are: ${Object.values(e).map(e=>e.path).join(", ")}`);return o}(t,n,e)}function f(e){return d(e).versions}function g(e){const t=d(e);return s(t)}function h(e){const t=d(e),{pathname:n}=(0,r.zy)();return c(t,n)}function m(e){const t=d(e),{pathname:n}=(0,r.zy)();return function(e,t){const n=s(e);return{latestDocSuggestion:c(e,t).alternateDocVersions[n.name],latestVersionSuggestion:n}}(t,n)}},40505(e,t,n){"use strict";n.r(t),n.d(t,{default:()=>r});const r={onRouteDidUpdate({location:e,previousLocation:t}){!t||e.pathname===t.pathname&&e.search===t.search&&e.hash===t.hash||setTimeout(()=>{window.gtag("set","page_path",e.pathname+e.search+e.hash),window.gtag("event","page_view")})}}},2415(e,t,n){"use strict";function r(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,e.__proto__=t}function a(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function o(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(){return i=Object.assign||function(e){for(var t=1;to});var r=n(58744),a=n.n(r);a().configure({showSpinner:!1});const o={onRouteUpdate({location:e,previousLocation:t}){if(t&&e.pathname!==t.pathname){const e=window.setTimeout(()=>{a().start()},200);return()=>window.clearTimeout(e)}},onRouteDidUpdate(){a().done()}}},55273(e,t,n){"use strict";var r=n(16194),a=n(4784);!function(e){const{themeConfig:{prism:t}}=a.default,{additionalLanguages:r}=t;globalThis.Prism=e,r.forEach(e=>{"php"===e&&n(88020),n(92310)(`./prism-${e}`)}),delete globalThis.Prism}(r.My)},72018(e,t,n){"use strict";n.d(t,{A:()=>d});n(30758);var r=n(13526),a=n(61288),o=n(91878),i=n(46753),s=n(86820);const c="anchorWithStickyNavbar_NfP0",l="anchorWithHideOnScrollNavbar_D_uw";var u=n(86070);function d({as:e,id:t,...n}){const d=(0,s.A)(),{navbar:{hideOnScroll:p}}=(0,o.p)();if("h1"===e||!t)return(0,u.jsx)(e,{...n,id:void 0});d.collectAnchor(t);const f=(0,a.T)({id:"theme.common.headingLinkTitle",message:"Direct link to {heading}",description:"Title for link to heading"},{heading:"string"==typeof n.children?n.children:t});return(0,u.jsxs)(e,{...n,className:(0,r.A)("anchor",p?l:c,n.className),id:t,children:[n.children,(0,u.jsx)(i.A,{className:"hash-link",to:`#${t}`,"aria-label":f,title:f,children:"\u200b"})]})}},39283(e,t,n){"use strict";n.d(t,{A:()=>o});n(30758);const r="iconExternalLink_dRok";var a=n(86070);function o({width:e=13.5,height:t=13.5}){return(0,a.jsx)("svg",{width:e,height:t,"aria-hidden":"true",viewBox:"0 0 24 24",className:r,children:(0,a.jsx)("path",{fill:"currentColor",d:"M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"})})}},44820(e,t,n){"use strict";n.d(t,{A:()=>$t});var r=n(30758),a=n(13526),o=n(88233),i=n(87712),s=n(25557),c=n(61288),l=n(52438),u=n(86070);const d="__docusaurus_skipToContent_fallback";function p(e){e.setAttribute("tabindex","-1"),e.focus(),e.removeAttribute("tabindex")}function f(){const e=(0,r.useRef)(null),{action:t}=(0,s.W6)(),n=(0,r.useCallback)(e=>{e.preventDefault();const t=document.querySelector("main:first-of-type")??document.getElementById(d);t&&p(t)},[]);return(0,l.$)(({location:n})=>{e.current&&!n.hash&&"PUSH"===t&&p(e.current)}),{containerRef:e,onClick:n}}const g=(0,c.T)({id:"theme.common.skipToMainContent",description:"The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation",message:"Skip to main content"});function h(e){const t=e.children??g,{containerRef:n,onClick:r}=f();return(0,u.jsx)("div",{ref:n,role:"region","aria-label":g,children:(0,u.jsx)("a",{...e,href:`#${d}`,onClick:r,children:t})})}var m=n(46103),b=n(23555);const y="skipToContent_E2xz";function v(){return(0,u.jsx)(h,{className:y})}var x=n(91878),w=n(68945),S=n(68835);const k="content_zf74";function _(e){const{announcementBar:t}=(0,x.p)(),{content:n}=t;return(0,u.jsx)("div",{...e,className:(0,S.A)(k,e.className),dangerouslySetInnerHTML:{__html:n}})}const E="announcementWrapper_Ma07",C="announcementBar_s0pr",T="announcementBarClose_iXyO",A="announcementBarContent_dpRF";function P(){const{announcementBar:e}=(0,x.p)(),{isActive:t,close:n}=(0,w.M)();if(!t)return null;const{backgroundColor:r,textColor:a,isCloseable:o}=e;return(0,u.jsx)("div",{className:E,children:(0,u.jsxs)("div",{className:C,style:{backgroundColor:r,color:a},role:"banner",children:[o&&(0,u.jsx)("div",{onClick:n,className:T,"aria-label":"Close",children:"\xd7"}),(0,u.jsx)(_,{className:A})]})})}var N=n(85013),j=n(32416);var L=n(5468),O=n(12496);const R=r.createContext(null);function I({children:e}){const t=function(){const e=(0,N.M)(),t=(0,O.YL)(),[n,a]=(0,r.useState)(!1),o=null!==t.component,i=(0,L.ZC)(o);return(0,r.useEffect)(()=>{o&&!i&&a(!0)},[o,i]),(0,r.useEffect)(()=>{o?e.shown||a(!0):a(!1)},[e.shown,o]),(0,r.useMemo)(()=>[n,a],[n])}();return(0,u.jsx)(R.Provider,{value:t,children:e})}function D(e){if(e.component){const t=e.component;return(0,u.jsx)(t,{...e.props})}}function F(){const e=(0,r.useContext)(R);if(!e)throw new L.dV("NavbarSecondaryMenuDisplayProvider");const[t,n]=e,a=(0,r.useCallback)(()=>n(!1),[n]),o=(0,O.YL)();return(0,r.useMemo)(()=>({shown:t,hide:a,content:D(o)}),[a,o,t])}function M({header:e,primaryMenu:t,secondaryMenu:n}){const{shown:r}=F();return(0,u.jsxs)("div",{className:"navbar-sidebar",children:[e,(0,u.jsxs)("div",{className:(0,S.A)("navbar-sidebar__items",{"navbar-sidebar__items--show-secondary":r}),children:[(0,u.jsx)("div",{className:"navbar-sidebar__item menu",children:t}),(0,u.jsx)("div",{className:"navbar-sidebar__item menu",children:n})]})]})}var z=n(75261),$=n(81264);function B(e){return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:24,height:24,...e,children:(0,u.jsx)("path",{fill:"currentColor",d:"M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"})})}function U(e){return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:24,height:24,...e,children:(0,u.jsx)("path",{fill:"currentColor",d:"M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"})})}const q={toggle:"toggle_x5Bc",toggleButton:"toggleButton_Eozw",darkToggleIcon:"darkToggleIcon_hZjB",lightToggleIcon:"lightToggleIcon_gu6m",toggleButtonDisabled:"toggleButtonDisabled_LJlu"};function H({className:e,buttonClassName:t,value:n,onChange:r}){const o=(0,$.A)(),i=(0,c.T)({message:"Switch between dark and light mode (currently {mode})",id:"theme.colorToggle.ariaLabel",description:"The ARIA label for the navbar color mode toggle"},{mode:"dark"===n?(0,c.T)({message:"dark mode",id:"theme.colorToggle.ariaLabel.mode.dark",description:"The name for the dark color mode"}):(0,c.T)({message:"light mode",id:"theme.colorToggle.ariaLabel.mode.light",description:"The name for the light color mode"})});return(0,u.jsx)("div",{className:(0,a.A)(q.toggle,e),children:(0,u.jsxs)("button",{className:(0,a.A)("clean-btn",q.toggleButton,!o&&q.toggleButtonDisabled,t),type:"button",onClick:()=>r("dark"===n?"light":"dark"),disabled:!o,title:i,"aria-label":i,"aria-live":"polite",children:[(0,u.jsx)(B,{className:(0,a.A)(q.toggleIcon,q.lightToggleIcon)}),(0,u.jsx)(U,{className:(0,a.A)(q.toggleIcon,q.darkToggleIcon)})]})})}const Q=r.memo(H),V="darkNavbarColorModeToggle_Q0Zn";function W({className:e}){const t=(0,x.p)().navbar.style,n=(0,x.p)().colorMode.disableSwitch,{colorMode:r,setColorMode:a}=(0,z.G)();return n?null:(0,u.jsx)(Q,{className:e,buttonClassName:"dark"===t?V:void 0,value:r,onChange:a})}function G({width:e=21,height:t=21,color:n="currentColor",strokeWidth:r=1.2,className:a,...o}){return(0,u.jsx)("svg",{viewBox:"0 0 15 15",width:e,height:t,...o,children:(0,u.jsx)("g",{stroke:n,strokeWidth:r,children:(0,u.jsx)("path",{d:"M.75.75l13.5 13.5M14.25.75L.75 14.25"})})})}var Y=n(7930);function K(){return(0,u.jsx)(Y.A,{className:"navbar__brand",imageClassName:"navbar__logo",titleClassName:"navbar__title text--truncate"})}const Z="colorModeToggle_ECYP";function X(){const e=(0,N.M)();return(0,u.jsx)("button",{type:"button","aria-label":(0,c.T)({id:"theme.docs.sidebar.closeSidebarButtonAriaLabel",message:"Close navigation bar",description:"The ARIA label for close button of mobile sidebar"}),className:"clean-btn navbar-sidebar__close",onClick:()=>e.toggle(),children:(0,u.jsx)(G,{color:"var(--ifm-color-emphasis-600)"})})}function J(){return(0,u.jsxs)("div",{className:"navbar-sidebar__brand",children:[(0,u.jsx)(K,{}),(0,u.jsx)(W,{className:"margin-right--md "+Z}),(0,u.jsx)(X,{})]})}var ee=n(46753),te=n(72646),ne=n(6887);function re(e,t){return void 0!==e&&void 0!==t&&new RegExp(e,"gi").test(t)}var ae=n(39283);function oe({activeBasePath:e,activeBaseRegex:t,to:n,href:r,label:a,html:o,isDropdownLink:i,prependBaseUrlToHref:s,...c}){const l=(0,te.Ay)(n),d=(0,te.Ay)(e),p=(0,te.Ay)(r,{forcePrependBaseUrl:!0}),f=a&&r&&!(0,ne.A)(r),g=o?{dangerouslySetInnerHTML:{__html:o}}:{children:(0,u.jsxs)(u.Fragment,{children:[a,f&&(0,u.jsx)(ae.A,{...i&&{width:12,height:12}})]})};return r?(0,u.jsx)(ee.A,{href:s?p:r,...c,...g}):(0,u.jsx)(ee.A,{to:l,isNavLink:!0,...(e||t)&&{isActive:(e,n)=>t?re(t,n.pathname):n.pathname.startsWith(d)},...c,...g})}function ie({className:e,isDropdownItem:t=!1,...n}){const r=(0,u.jsx)(oe,{className:(0,a.A)(t?"dropdown__link":"navbar__item navbar__link",e),isDropdownLink:t,...n});return t?(0,u.jsx)("li",{children:r}):r}function se({className:e,isDropdownItem:t,...n}){return(0,u.jsx)("li",{className:"menu__list-item",children:(0,u.jsx)(oe,{className:(0,a.A)("menu__link",e),...n})})}function ce({mobile:e=!1,position:t,...n}){const r=e?se:ie;return(0,u.jsx)(r,{...n,activeClassName:n.activeClassName??(e?"menu__link--active":"navbar__link--active")})}var le=n(74286),ue=n(37505),de=n(90525);const pe="dropdownNavbarItemMobile_sTtj";function fe(e,t){return e.some(e=>function(e,t){return!!(0,ue.ys)(e.to,t)||!!re(e.activeBaseRegex,t)||!(!e.activeBasePath||!t.startsWith(e.activeBasePath))}(e,t))}function ge({items:e,position:t,className:n,onClick:o,...i}){const s=(0,r.useRef)(null),[c,l]=(0,r.useState)(!1);return(0,r.useEffect)(()=>{const e=e=>{s.current&&!s.current.contains(e.target)&&l(!1)};return document.addEventListener("mousedown",e),document.addEventListener("touchstart",e),document.addEventListener("focusin",e),()=>{document.removeEventListener("mousedown",e),document.removeEventListener("touchstart",e),document.removeEventListener("focusin",e)}},[s]),(0,u.jsxs)("div",{ref:s,className:(0,a.A)("navbar__item","dropdown","dropdown--hoverable",{"dropdown--right":"right"===t,"dropdown--show":c}),children:[(0,u.jsx)(oe,{"aria-haspopup":"true","aria-expanded":c,role:"button",href:i.to?void 0:"#",className:(0,a.A)("navbar__link",n),...i,onClick:i.to?void 0:e=>e.preventDefault(),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),l(!c))},children:i.children??i.label}),(0,u.jsx)("ul",{className:"dropdown__menu",children:e.map((e,t)=>(0,r.createElement)(gt,{isDropdownItem:!0,activeClassName:"dropdown__link--active",...e,key:t}))})]})}function he({items:e,className:t,position:n,onClick:o,...i}){const c=function(){const{siteConfig:{baseUrl:e}}=(0,de.A)(),{pathname:t}=(0,s.zy)();return t.replace(e,"/")}(),l=fe(e,c),{collapsed:d,toggleCollapsed:p,setCollapsed:f}=(0,le.u)({initialState:()=>!l});return(0,r.useEffect)(()=>{l&&f(!l)},[c,l,f]),(0,u.jsxs)("li",{className:(0,a.A)("menu__list-item",{"menu__list-item--collapsed":d}),children:[(0,u.jsx)(oe,{role:"button",className:(0,a.A)(pe,"menu__link menu__link--sublist menu__link--sublist-caret",t),...i,onClick:e=>{e.preventDefault(),p()},children:i.children??i.label}),(0,u.jsx)(le.N,{lazy:!0,as:"ul",className:"menu__list",collapsed:d,children:e.map((e,t)=>(0,r.createElement)(gt,{mobile:!0,isDropdownItem:!0,onClick:o,activeClassName:"menu__link--active",...e,key:t}))})]})}function me({mobile:e=!1,...t}){const n=e?he:ge;return(0,u.jsx)(n,{...t})}var be=n(99555);function ye({width:e=20,height:t=20,...n}){return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:e,height:t,"aria-hidden":!0,...n,children:(0,u.jsx)("path",{fill:"currentColor",d:"M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"})})}const ve="iconLanguage_hPI9";var xe=n(79905),we=n(33453),Se=n(72631),ke=n(16451),_e=n(68981),Ee=n(37753),Ce=n(62391),Te=n(24442),Ae=n(72313),Pe=n(70406);const Ne="searchBar_rYdB",je="dropdownMenu_T3_r",Le="searchBarLeft_nMJi",Oe="suggestion_Opf3",Re="cursor_Qg3z",Ie="hitTree_Mkai",De="hitIcon_khtE",Fe="hitPath_vvtl",Me="noResultsIcon_CoNP",ze="hitFooter_nnuh",$e="hitWrapper_epee",Be="hitTitle_CZbN",Ue="hitAction_LxJZ",qe="noResults_gEj3",He="searchBarContainer_fbMQ",Qe="searchBarLoadingRing_JyUX",Ve="searchClearButton_Hwt3",We="searchIndexLoading_U9jB",Ge="searchHintContainer_s8dw",Ye="searchHint_ygwY",Ke="focused_CeIh",Ze="input_k6hQ",Xe="hint_zOoN",Je="suggestions_iGJ1",et="dataset_uhDD",tt="empty_tpcH";function nt({document:e,type:t,page:n,metadata:r,tokens:a,isInterOfTree:o,isLastOfTree:i}){const s=t===Ee.i.Title,c=t===Ee.i.Keywords,l=s||c,u=t===Ee.i.Heading,d=[];o?d.push(''):i&&d.push('');const p=d.map(e=>`${e}`),f=`${l?'':u?'':''}`,g=[`${c?(0,Ae.Z)(e.s,a):(0,Pe.C)(e.t,(0,Te.g)(r,"t"),a)}`];if(!o&&!i&&xe.tb){const t=n?n.b?.concat(n.t).concat(e.s&&e.s!==n.t?e.s:[]):e.b;g.push(`${(0,Ce.$)(t??[])}`)}else l||g.push(`${(0,Ae.Z)(n.t||(e.u.startsWith("/docs/api-reference/")?"API Reference":""),a)}`);const h=``;return[...p,f,``,...g,"",h].join("")}function rt(){return`${(0,c.T)({id:"theme.SearchBar.noResultsText",message:"No results"})}`}var at=n(93939),ot=n(57433);async function it(){const e=await Promise.all([n.e(9951),n.e(9056)]).then(n.t.bind(n,29951,23)),t=e.default;return t.noConflict?t.noConflict():e.noConflict&&e.noConflict(),t}const st="_highlight";const ct=function({handleSearchBarToggle:e}){const t=(0,$.A)(),{siteConfig:{baseUrl:n},i18n:{currentLocale:a}}=(0,de.A)(),o=(0,Se.vT)();let i=n;try{const{preferredVersion:e}=(0,we.g1)(o?.pluginId??xe.UB);e&&!e.isLast&&(i=e.path+"/")}catch(F){if(xe.I$&&!(F instanceof L.dV))throw F}const l=(0,s.W6)(),d=(0,s.zy)(),p=(0,r.useRef)(null),f=(0,r.useRef)(new Map),g=(0,r.useRef)(!1),[h,m]=(0,r.useState)(!1),[b,y]=(0,r.useState)(!1),[v,x]=(0,r.useState)(""),w=(0,r.useRef)(null),k=(0,r.useRef)(""),[_,E]=(0,r.useState)("");(0,r.useEffect)(()=>{if(!Array.isArray(xe.Hg))return;let e="";if(d.pathname.startsWith(i)){const t=d.pathname.substring(i.length);let n;for(const e of xe.Hg){const r="string"==typeof e?e:e.path;if(t===r||t.startsWith(`${r}/`)){n=r;break}}n&&(e=n)}k.current!==e&&(f.current.delete(e),k.current=e),E(e)},[d.pathname,i]);const C=!!xe.O6&&Array.isArray(xe.Hg)&&""===_,T=(0,r.useCallback)(async()=>{if(C||f.current.get(_))return;f.current.set(_,"loading"),w.current?.autocomplete.destroy(),m(!0);const[{wrappedIndexes:e,zhDictionary:t},r]=await Promise.all([(0,ke.Z)(i,_),it()]);if(w.current=r(p.current,{hint:!1,autoselect:!0,openOnFocus:!0,cssClasses:{root:(0,S.A)(Ne,{[Le]:"left"===xe.ZG}),noPrefix:!0,dropdownMenu:je,input:Ze,hint:Xe,suggestions:Je,suggestion:Oe,cursor:Re,dataset:et,empty:tt}},[{source:(0,_e.m)(e,t,xe.AT),templates:{suggestion:nt,empty:rt,footer:({query:e,isEmpty:t})=>{if(t&&(!_||!xe.dz))return;const r=(({query:e,isEmpty:t})=>{const r=document.createElement("a"),o=new URLSearchParams;let s;if(o.set("q",e),_){const e=_&&Array.isArray(xe.Hg)?xe.Hg.find(e=>"string"==typeof e?e===_:e.path===_):_,n=e?(0,ot.p)(e,a).label:_;s=xe.dz&&t?(0,c.T)({id:"theme.SearchBar.seeAllOutsideContext",message:'See all results outside "{context}"'},{context:n}):(0,c.T)({id:"theme.SearchBar.searchInContext",message:'See all results within "{context}"'},{context:n})}else s=(0,c.T)({id:"theme.SearchBar.seeAll",message:"See all results"});if(!_||!Array.isArray(xe.Hg)||xe.dz&&t||o.set("ctx",_),i!==n){if(!i.startsWith(n))throw new Error(`Version url '${i}' does not start with base url '${n}', this is a bug of \`@easyops-cn/docusaurus-search-local\`, please report it.`);o.set("version",i.substring(n.length))}const u=`${n}search/?${o.toString()}`;return r.href=u,r.textContent=s,r.addEventListener("click",e=>{e.ctrlKey||e.metaKey||(e.preventDefault(),w.current?.autocomplete.close(),l.push(u))}),r})({query:e,isEmpty:t}),o=document.createElement("div");return o.className=ze,o.appendChild(r),o}}}]).on("autocomplete:selected",function(e,{document:{u:t,h:n},tokens:r}){p.current?.blur();let a=t;if(xe.CU&&r.length>0){const e=new URLSearchParams;for(const t of r)e.append(st,t);a+=`?${e.toString()}`}n&&(a+=n),l.push(a)}).on("autocomplete:closed",()=>{p.current?.blur()}),f.current.set(_,"done"),m(!1),g.current){const e=p.current;e.value&&w.current?.autocomplete.open(),e.focus()}},[C,_,i,n,l]);(0,r.useEffect)(()=>{if(!xe.CU)return;const e=t?new URLSearchParams(d.search).getAll(st):[];setTimeout(()=>{const t=document.querySelector("article");if(!t)return;const n=new xe.CU(t);n.unmark(),0!==e.length&&n.mark(e),x(e.join(" ")),w.current?.autocomplete.setVal(e.join(" "))})},[t,d.search,d.pathname]);const[A,P]=(0,r.useState)(!1),N=(0,r.useCallback)(()=>{g.current=!0,T(),P(!0),e?.(!0)},[e,T]),j=(0,r.useCallback)(()=>{P(!1),e?.(!1)},[e]),O=(0,r.useCallback)(()=>{T()},[T]),R=(0,r.useCallback)(e=>{x(e.target.value),e.target.value&&y(!0)},[]),I=!!t&&/mac/i.test(navigator.userAgentData?.platform??navigator.platform);(0,r.useEffect)(()=>{if(!xe.WW)return;const e=e=>{!(I?e.metaKey:e.ctrlKey)||"k"!==e.key&&"K"!==e.key||(e.preventDefault(),p.current?.focus(),N())};return document.addEventListener("keydown",e),()=>{document.removeEventListener("keydown",e)}},[I,N]);const D=(0,r.useCallback)(()=>{const e=new URLSearchParams(d.search);e.delete(st);const t=e.toString(),n=d.pathname+(""!=t?`?${t}`:"")+d.hash;n!=d.pathname+d.search+d.hash&&l.push(n),x(""),w.current?.autocomplete.setVal("")},[d.pathname,d.search,d.hash,l]);return(0,u.jsxs)("div",{className:(0,S.A)("navbar__search",He,{[We]:h&&b,[Ke]:A}),hidden:C,dir:"ltr",children:[(0,u.jsx)("input",{placeholder:(0,c.T)({id:"theme.SearchBar.label",message:"Search",description:"The ARIA label and placeholder for search button"}),"aria-label":"Search",className:"navbar__search-input",onMouseEnter:O,onFocus:N,onBlur:j,onChange:R,ref:p,value:v}),(0,u.jsx)(at.A,{className:Qe}),xe.WW&&xe.pk&&(""!==v?(0,u.jsx)("button",{className:Ve,onClick:D,children:"\u2715"}):t&&(0,u.jsxs)("div",{className:Ge,children:[(0,u.jsx)("kbd",{className:Ye,children:I?"\u2318":"ctrl"}),(0,u.jsx)("kbd",{className:Ye,children:"K"})]}))]})},lt="navbarSearchContainer_dCNk";function ut({children:e,className:t}){return(0,u.jsx)("div",{className:(0,S.A)(t,lt),children:e})}var dt=n(64878);const pt=e=>e.docs.find(t=>t.id===e.mainDocId);const ft={default:ce,localeDropdown:function({mobile:e,dropdownItemsBefore:t,dropdownItemsAfter:n,queryString:r="",...a}){const{i18n:{currentLocale:o,locales:i,localeConfigs:l}}=(0,de.A)(),d=(0,be.o)(),{search:p,hash:f}=(0,s.zy)(),g=[...t,...i.map(t=>{const n=`${`pathname://${d.createUrl({locale:t,fullyQualified:!1})}`}${p}${f}${r}`;return{label:l[t].label,lang:l[t].htmlLang,to:n,target:"_self",autoAddBaseUrl:!1,className:t===o?e?"menu__link--active":"dropdown__link--active":""}}),...n],h=e?(0,c.T)({message:"Languages",id:"theme.navbar.mobileLanguageDropdown.label",description:"The label for the mobile language switcher dropdown"}):l[o].label;return(0,u.jsx)(me,{...a,mobile:e,label:(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(ye,{className:ve}),h]}),items:g})},search:function({mobile:e,className:t}){return e?null:(0,u.jsx)(ut,{className:t,children:(0,u.jsx)(ct,{})})},dropdown:me,html:function({value:e,className:t,mobile:n=!1,isDropdownItem:r=!1}){const o=r?"li":"div";return(0,u.jsx)(o,{className:(0,a.A)({navbar__item:!n&&!r,"menu__list-item":n},t),dangerouslySetInnerHTML:{__html:e}})},doc:function({docId:e,label:t,docsPluginId:n,...r}){const{activeDoc:a}=(0,Se.zK)(n),o=(0,dt.QB)(e,n),i=a?.path===o?.path;return null===o||o.unlisted&&!i?null:(0,u.jsx)(ce,{exact:!0,...r,isActive:()=>i||!!a?.sidebar&&a.sidebar===o.sidebar,label:t??o.id,to:o.path})},docSidebar:function({sidebarId:e,label:t,docsPluginId:n,...r}){const{activeDoc:a}=(0,Se.zK)(n),o=(0,dt.fW)(e,n).link;if(!o)throw new Error(`DocSidebarNavbarItem: Sidebar with ID "${e}" doesn't have anything to be linked to.`);return(0,u.jsx)(ce,{exact:!0,...r,isActive:()=>a?.sidebar===e,label:t??o.label,to:o.path})},docsVersion:function({label:e,to:t,docsPluginId:n,...r}){const a=(0,dt.Vd)(n)[0],o=e??a.label,i=t??(e=>e.docs.find(t=>t.id===e.mainDocId))(a).path;return(0,u.jsx)(ce,{...r,label:o,to:i})},docsVersionDropdown:function({mobile:e,docsPluginId:t,dropdownActiveClassDisabled:n,dropdownItemsBefore:r,dropdownItemsAfter:a,...o}){const{search:i,hash:l}=(0,s.zy)(),d=(0,Se.zK)(t),p=(0,Se.jh)(t),{savePreferredVersionName:f}=(0,we.g1)(t),g=[...r,...p.map(e=>{const t=d.alternateDocVersions[e.name]??pt(e);return{label:e.label,to:`${t.path}${i}${l}`,isActive:()=>e===d.activeVersion,onClick:()=>f(e.name)}}),...a],h=(0,dt.Vd)(t)[0],m=e&&g.length>1?(0,c.T)({id:"theme.navbar.mobileVersionsDropdown.label",message:"Versions",description:"The label for the navbar versions dropdown on mobile view"}):h.label,b=e&&g.length>1?void 0:pt(h).path;return g.length<=1?(0,u.jsx)(ce,{...o,mobile:e,label:m,to:b,isActive:n?()=>!1:void 0}):(0,u.jsx)(me,{...o,mobile:e,label:m,to:b,items:g,isActive:n?()=>!1:void 0})}};function gt({type:e,...t}){const n=function(e,t){return e&&"default"!==e?e:"items"in t?"dropdown":"default"}(e,t),r=ft[n];if(!r)throw new Error(`No NavbarItem component found for type "${e}".`);return(0,u.jsx)(r,{...t})}function ht(){const e=(0,N.M)(),t=(0,x.p)().navbar.items;return(0,u.jsx)("ul",{className:"menu__list",children:t.map((t,n)=>(0,r.createElement)(gt,{mobile:!0,...t,onClick:()=>e.toggle(),key:n}))})}function mt(e){return(0,u.jsx)("button",{...e,type:"button",className:"clean-btn navbar-sidebar__back",children:(0,u.jsx)(c.A,{id:"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel",description:"The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)",children:"\u2190 Back to main menu"})})}function bt(){const e=0===(0,x.p)().navbar.items.length,t=F();return(0,u.jsxs)(u.Fragment,{children:[!e&&(0,u.jsx)(mt,{onClick:()=>t.hide()}),t.content]})}function yt(){const e=(0,N.M)();return function(e=!0){(0,r.useEffect)(()=>(document.body.style.overflow=e?"hidden":"visible",()=>{document.body.style.overflow="visible"}),[e])}(e.shown),e.shouldRender?(0,u.jsx)(M,{header:(0,u.jsx)(J,{}),primaryMenu:(0,u.jsx)(ht,{}),secondaryMenu:(0,u.jsx)(bt,{})}):null}const vt="navbarHideable_jvwV",xt="navbarHidden_nLSi";function wt(e){return(0,u.jsx)("div",{role:"presentation",...e,className:(0,S.A)("navbar-sidebar__backdrop",e.className)})}function St({children:e}){const{navbar:{hideOnScroll:t,style:n}}=(0,x.p)(),a=(0,N.M)(),{navbarRef:o,isNavbarVisible:i}=function(e){const[t,n]=(0,r.useState)(e),a=(0,r.useRef)(!1),o=(0,r.useRef)(0),i=(0,r.useCallback)(e=>{null!==e&&(o.current=e.getBoundingClientRect().height)},[]);return(0,j.Mq)(({scrollY:t},r)=>{if(!e)return;if(t=i?n(!1):t+c{if(!e)return;const r=t.location.hash;if(r?document.getElementById(r.substring(1)):void 0)return a.current=!0,void n(!1);n(!0)}),{navbarRef:i,isNavbarVisible:t}}(t);return(0,u.jsxs)("nav",{ref:o,"aria-label":(0,c.T)({id:"theme.NavBar.navAriaLabel",message:"Main",description:"The ARIA label for the main navigation"}),className:(0,S.A)("navbar","navbar--fixed-top",t&&[vt,!i&&xt],{"navbar--dark":"dark"===n,"navbar--primary":"primary"===n,"navbar-sidebar--show":a.shown}),children:[e,(0,u.jsx)(wt,{onClick:a.toggle}),(0,u.jsx)(yt,{})]})}var kt=n(45220);function _t({width:e=30,height:t=30,className:n,...r}){return(0,u.jsx)("svg",{className:n,width:e,height:t,viewBox:"0 0 30 30","aria-hidden":"true",...r,children:(0,u.jsx)("path",{stroke:"currentColor",strokeLinecap:"round",strokeMiterlimit:"10",strokeWidth:"2",d:"M4 7h22M4 15h22M4 23h22"})})}function Et(){const{toggle:e,shown:t}=(0,N.M)();return(0,u.jsx)("button",{onClick:e,"aria-label":(0,c.T)({id:"theme.docs.sidebar.toggleSidebarButtonAriaLabel",message:"Toggle navigation bar",description:"The ARIA label for hamburger menu button of mobile navigation"}),"aria-expanded":t,className:"navbar__toggle clean-btn",type:"button",children:(0,u.jsx)(_t,{})})}const Ct="colorModeToggle_x44X",Tt="dividerLine_MByL";function At({items:e}){return(0,u.jsx)(u.Fragment,{children:e.map((e,t)=>(0,u.jsx)(kt.k2,{onError:t=>new Error(`A theme navbar item failed to render.\nPlease double-check the following navbar item (themeConfig.navbar.items) of your Docusaurus config:\n${JSON.stringify(e,null,2)}`,{cause:t}),children:(0,u.jsx)(gt,{...e})},t))})}function Pt({left:e,right:t,searchBarItem:n}){return(0,u.jsxs)("div",{className:"navbar__inner",children:[(0,u.jsx)("div",{className:"navbar__items",children:e}),!n&&(0,u.jsx)(ut,{children:(0,u.jsx)(ct,{})}),(0,u.jsx)("div",{className:"navbar__items navbar__items--right",children:t})]})}function Nt(){const e=(0,N.M)(),t=(0,x.p)().navbar.items,[n,r]=function(e){function t(e){return"left"===(e.position??"right")}return[e.filter(t),e.filter(e=>!t(e))]}(t);t.find(e=>"search"===e.type);return(0,u.jsx)(Pt,{left:(0,u.jsxs)(u.Fragment,{children:[!e.disabled&&(0,u.jsx)(Et,{}),(0,u.jsx)(K,{}),(0,u.jsx)("div",{className:Tt,children:"|"}),(0,u.jsx)(At,{items:n}),(0,u.jsx)("div",{className:"",children:(0,u.jsx)(W,{className:Ct})})]}),right:(0,u.jsx)(u.Fragment,{children:(0,u.jsx)(At,{items:r})})})}function jt(){return(0,u.jsx)(St,{children:(0,u.jsx)(Nt,{})})}var Lt=n(17101);function Ot(){return(0,u.jsx)(Lt.A,{})}const Rt=r.memo(Ot),It=(0,L.fM)([z.a,w.o,j.Tv,we.VQ,i.Jx,function({children:e}){return(0,u.jsx)(O.y_,{children:(0,u.jsx)(N.e,{children:(0,u.jsx)(I,{children:e})})})}]);function Dt({children:e}){return(0,u.jsx)(It,{children:e})}var Ft=n(72018);function Mt({error:e,tryAgain:t}){return(0,u.jsx)("main",{className:"container margin-vert--xl",children:(0,u.jsx)("div",{className:"row",children:(0,u.jsxs)("div",{className:"col col--6 col--offset-3",children:[(0,u.jsx)(Ft.A,{as:"h1",className:"hero__title",children:(0,u.jsx)(c.A,{id:"theme.ErrorPageContent.title",description:"The title of the fallback page when the page crashed",children:"This page crashed."})}),(0,u.jsx)("div",{className:"margin-vert--lg",children:(0,u.jsx)(kt.a2,{onClick:t,className:"button button--primary shadow--lw"})}),(0,u.jsx)("hr",{}),(0,u.jsx)("div",{className:"margin-vert--md",children:(0,u.jsx)(kt.bq,{error:e})})]})})})}const zt="mainWrapper_nyBa";function $t(e){const{children:t,noFooter:n,wrapperClassName:r,title:s,description:c}=e;return(0,b.J)(),(0,u.jsxs)(Dt,{children:[(0,u.jsx)(i.be,{title:s,description:c}),(0,u.jsx)(v,{}),(0,u.jsx)(P,{}),(0,u.jsx)(jt,{}),(0,u.jsx)("div",{id:d,className:(0,a.A)(m.G.wrapper.main,zt,r),children:(0,u.jsx)(o.A,{fallback:e=>(0,u.jsx)(Mt,{...e}),children:t})}),!n&&(0,u.jsx)(Rt,{})]})}},7930(e,t,n){"use strict";n.d(t,{A:()=>m});var r=n(30758),a=n(46753),o=n(72646),i=n(90525),s=n(91878),c=n(13526),l=n(81264),u=n(75261);const d={themedComponent:"themedComponent_nudQ","themedComponent--light":"themedComponent--light_hm6B","themedComponent--dark":"themedComponent--dark_VByZ"};var p=n(86070);function f({className:e,children:t}){const n=(0,l.A)(),{colorMode:a}=(0,u.G)();return(0,p.jsx)(p.Fragment,{children:(n?"dark"===a?["dark"]:["light"]:["light","dark"]).map(n=>{const a=t({theme:n,className:(0,c.A)(e,d.themedComponent,d[`themedComponent--${n}`])});return(0,p.jsx)(r.Fragment,{children:a},n)})})}function g(e){const{sources:t,className:n,alt:r,...a}=e;return(0,p.jsx)(f,{className:n,children:({theme:e,className:n})=>(0,p.jsx)("img",{src:t[e],alt:r,className:n,...a})})}function h({logo:e,alt:t,imageClassName:n}){const r={light:(0,o.Ay)(e.src),dark:(0,o.Ay)(e.srcDark||e.src)},a=(0,p.jsx)(g,{className:e.className,sources:r,height:e.height,width:e.width,alt:t,style:e.style});return n?(0,p.jsx)("div",{className:n,children:a}):a}function m(e){const{siteConfig:{title:t}}=(0,i.A)(),{navbar:{title:n,logo:r}}=(0,s.p)(),{imageClassName:c,titleClassName:l,...u}=e,d=(0,o.Ay)(r?.href||"/"),f=n?"":t,g=r?.alt??f;return(0,p.jsxs)(a.A,{to:d,...u,...r?.target&&{target:r.target},children:[r&&(0,p.jsx)(h,{logo:r,alt:g,imageClassName:c}),null!=n&&(0,p.jsx)("b",{className:l,children:n})]})}},89500(e,t,n){"use strict";n.d(t,{A:()=>o});n(30758);var r=n(33959),a=n(86070);function o({locale:e,version:t,tag:n}){const o=e;return(0,a.jsxs)(r.A,{children:[e&&(0,a.jsx)("meta",{name:"docusaurus_locale",content:e}),t&&(0,a.jsx)("meta",{name:"docusaurus_version",content:t}),n&&(0,a.jsx)("meta",{name:"docusaurus_tag",content:n}),o&&(0,a.jsx)("meta",{name:"docsearch:language",content:o}),t&&(0,a.jsx)("meta",{name:"docsearch:version",content:t}),n&&(0,a.jsx)("meta",{name:"docsearch:docusaurus_tag",content:n})]})}},74286(e,t,n){"use strict";n.d(t,{N:()=>m,u:()=>c});var r=n(30758),a=n(11662),o=n(85924),i=n(44405),s=n(86070);function c({initialState:e}){const[t,n]=(0,r.useState)(e??!1),a=(0,r.useCallback)(()=>{n(e=>!e)},[]);return{collapsed:t,setCollapsed:n,toggleCollapsed:a}}const l={display:"none",overflow:"hidden",height:"0px"},u={display:"block",overflow:"visible",height:"auto"};function d(e,t){const n=t?l:u;e.style.display=n.display,e.style.overflow=n.overflow,e.style.height=n.height}function p({collapsibleRef:e,collapsed:t,animation:n}){const a=(0,r.useRef)(!1);(0,r.useEffect)(()=>{const r=e.current;function o(){const e=r.scrollHeight,t=n?.duration??function(e){if((0,i.O)())return 1;const t=e/36;return Math.round(10*(4+15*t**.25+t/5))}(e);return{transition:`height ${t}ms ${n?.easing??"ease-in-out"}`,height:`${e}px`}}function s(){const e=o();r.style.transition=e.transition,r.style.height=e.height}if(!a.current)return d(r,t),void(a.current=!0);return r.style.willChange="height",function(){const e=requestAnimationFrame(()=>{t?(s(),requestAnimationFrame(()=>{r.style.height=l.height,r.style.overflow=l.overflow})):(r.style.display="block",requestAnimationFrame(()=>{s()}))});return()=>cancelAnimationFrame(e)}()},[e,t,n])}function f(e){if(!a.A.canUseDOM)return e?l:u}function g({as:e="div",collapsed:t,children:n,animation:a,onCollapseTransitionEnd:o,className:i,disableSSRStyle:c}){const l=(0,r.useRef)(null);return p({collapsibleRef:l,collapsed:t,animation:a}),(0,s.jsx)(e,{ref:l,style:c?void 0:f(t),onTransitionEnd:e=>{"height"===e.propertyName&&(d(l.current,t),o?.(t))},className:i,children:n})}function h({collapsed:e,...t}){const[n,a]=(0,r.useState)(!e),[i,c]=(0,r.useState)(e);return(0,o.A)(()=>{e||a(!0)},[e]),(0,o.A)(()=>{n&&c(e)},[n,e]),n?(0,s.jsx)(g,{...t,collapsed:i}):null}function m({lazy:e,...t}){const n=e?h:g;return(0,s.jsx)(n,{...t})}},68945(e,t,n){"use strict";n.d(t,{M:()=>h,o:()=>g});var r=n(30758),a=n(81264),o=n(44118),i=n(5468),s=n(91878),c=n(86070);const l=(0,o.Wf)("docusaurus.announcement.dismiss"),u=(0,o.Wf)("docusaurus.announcement.id"),d=()=>"true"===l.get(),p=e=>l.set(String(e)),f=r.createContext(null);function g({children:e}){const t=function(){const{announcementBar:e}=(0,s.p)(),t=(0,a.A)(),[n,o]=(0,r.useState)(()=>!!t&&d());(0,r.useEffect)(()=>{o(d())},[]);const i=(0,r.useCallback)(()=>{p(!0),o(!0)},[]);return(0,r.useEffect)(()=>{if(!e)return;const{id:t}=e;let n=u.get();"annoucement-bar"===n&&(n="announcement-bar");const r=t!==n;u.set(t),r&&p(!1),!r&&d()||o(!1)},[e]),(0,r.useMemo)(()=>({isActive:!!e&&!n,close:i}),[e,n,i])}();return(0,c.jsx)(f.Provider,{value:t,children:e})}function h(){const e=(0,r.useContext)(f);if(!e)throw new i.dV("AnnouncementBarProvider");return e}},75261(e,t,n){"use strict";n.d(t,{G:()=>b,a:()=>m});var r=n(30758),a=n(11662),o=n(5468),i=n(44118),s=n(91878),c=n(86070);const l=r.createContext(void 0),u="theme",d=(0,i.Wf)(u),p="light",f="dark",g=e=>e===f?f:p;function h(){const{colorMode:{defaultMode:e,disableSwitch:t,respectPrefersColorScheme:n}}=(0,s.p)(),[o,i]=(0,r.useState)((e=>a.A.canUseDOM?g(document.documentElement.getAttribute("data-theme")):g(e))(e));(0,r.useEffect)(()=>{t&&d.del()},[t]);const c=(0,r.useCallback)((t,r={})=>{const{persist:a=!0}=r;t?(i(t),a&&(e=>{d.set(g(e))})(t)):(i(n?window.matchMedia("(prefers-color-scheme: dark)").matches?f:p:e),d.del())},[n,e]);(0,r.useEffect)(()=>{document.documentElement.setAttribute("data-theme",g(o))},[o]),(0,r.useEffect)(()=>{if(t)return;const e=e=>{if(e.key!==u)return;const t=d.get();null!==t&&c(g(t))};return window.addEventListener("storage",e),()=>window.removeEventListener("storage",e)},[t,c]);const l=(0,r.useRef)(!1);return(0,r.useEffect)(()=>{if(t&&!n)return;const e=window.matchMedia("(prefers-color-scheme: dark)"),r=()=>{window.matchMedia("print").matches||l.current?l.current=window.matchMedia("print").matches:c(null)};return e.addListener(r),()=>e.removeListener(r)},[c,t,n]),(0,r.useMemo)(()=>({colorMode:o,setColorMode:c,get isDarkTheme(){return o===f},setLightTheme(){c(p)},setDarkTheme(){c(f)}}),[o,c])}function m({children:e}){const t=h();return(0,c.jsx)(l.Provider,{value:t,children:e})}function b(){const e=(0,r.useContext)(l);if(null==e)throw new o.dV("ColorModeProvider","Please see https://docusaurus.io/docs/api/themes/configuration#use-color-mode.");return e}},33453(e,t,n){"use strict";n.d(t,{VQ:()=>y,g1:()=>x});var r=n(30758),a=n(72631),o=n(91260),i=n(91878),s=n(64878),c=n(5468),l=n(44118),u=n(86070);const d=e=>`docs-preferred-version-${e}`,p=(e,t,n)=>{(0,l.Wf)(d(e),{persistence:t}).set(n)},f=(e,t)=>(0,l.Wf)(d(e),{persistence:t}).get(),g=(e,t)=>{(0,l.Wf)(d(e),{persistence:t}).del()};const h=r.createContext(null);function m(){const e=(0,a.Gy)(),t=(0,i.p)().docs.versionPersistence,n=(0,r.useMemo)(()=>Object.keys(e),[e]),[o,s]=(0,r.useState)(()=>(e=>Object.fromEntries(e.map(e=>[e,{preferredVersionName:null}])))(n));(0,r.useEffect)(()=>{s(function({pluginIds:e,versionPersistence:t,allDocsData:n}){function r(e){const r=f(e,t);return n[e].versions.some(e=>e.name===r)?{preferredVersionName:r}:(g(e,t),{preferredVersionName:null})}return Object.fromEntries(e.map(e=>[e,r(e)]))}({allDocsData:e,versionPersistence:t,pluginIds:n}))},[e,t,n]);return[o,(0,r.useMemo)(()=>({savePreferredVersion:function(e,n){p(e,t,n),s(t=>({...t,[e]:{preferredVersionName:n}}))}}),[t])]}function b({children:e}){const t=m();return(0,u.jsx)(h.Provider,{value:t,children:e})}function y({children:e}){return s.C5?(0,u.jsx)(b,{children:e}):(0,u.jsx)(u.Fragment,{children:e})}function v(){const e=(0,r.useContext)(h);if(!e)throw new c.dV("DocsPreferredVersionContextProvider");return e}function x(e=o.W){const t=(0,a.ht)(e),[n,i]=v(),{preferredVersionName:s}=n[e];return{preferredVersion:t.versions.find(e=>e.name===s)??null,savePreferredVersionName:(0,r.useCallback)(t=>{i.savePreferredVersion(e,t)},[i,e])}}},66556(e,t,n){"use strict";n.d(t,{V:()=>c,t:()=>l});var r=n(30758),a=n(5468),o=n(86070);const i=Symbol("EmptyContext"),s=r.createContext(i);function c({children:e,name:t,items:n}){const a=(0,r.useMemo)(()=>t&&n?{name:t,items:n}:null,[t,n]);return(0,o.jsx)(s.Provider,{value:a,children:e})}function l(){const e=(0,r.useContext)(s);if(e===i)throw new a.dV("DocsSidebarProvider");return e}},52092(e,t,n){"use strict";n.d(t,{n:()=>s,r:()=>c});var r=n(30758),a=n(5468),o=n(86070);const i=r.createContext(null);function s({children:e,version:t}){return(0,o.jsx)(i.Provider,{value:t,children:e})}function c(){const e=(0,r.useContext)(i);if(null===e)throw new a.dV("DocsVersionProvider");return e}},85013(e,t,n){"use strict";n.d(t,{M:()=>f,e:()=>p});var r=n(30758),a=n(12496),o=n(65029),i=n(64493),s=n(91878),c=n(5468),l=n(86070);const u=r.createContext(void 0);function d(){const e=function(){const e=(0,a.YL)(),{items:t}=(0,s.p)().navbar;return 0===t.length&&!e.component}(),t=(0,o.l)(),n=!e&&"mobile"===t,[c,l]=(0,r.useState)(!1);(0,i.$Z)(()=>{if(c)return l(!1),!1});const u=(0,r.useCallback)(()=>{l(e=>!e)},[]);return(0,r.useEffect)(()=>{"desktop"===t&&l(!1)},[t]),(0,r.useMemo)(()=>({disabled:e,shouldRender:n,toggle:u,shown:c}),[e,n,u,c])}function p({children:e}){const t=d();return(0,l.jsx)(u.Provider,{value:t,children:e})}function f(){const e=r.useContext(u);if(void 0===e)throw new c.dV("NavbarMobileSidebarProvider");return e}},12496(e,t,n){"use strict";n.d(t,{GX:()=>l,YL:()=>c,y_:()=>s});var r=n(30758),a=n(5468),o=n(86070);const i=r.createContext(null);function s({children:e}){const t=(0,r.useState)({component:null,props:null});return(0,o.jsx)(i.Provider,{value:t,children:e})}function c(){const e=(0,r.useContext)(i);if(!e)throw new a.dV("NavbarSecondaryMenuContentProvider");return e[0]}function l({component:e,props:t}){const n=(0,r.useContext)(i);if(!n)throw new a.dV("NavbarSecondaryMenuContentProvider");const[,o]=n,s=(0,a.Be)(t);return(0,r.useEffect)(()=>{o({component:e,props:s})},[o,e,s]),(0,r.useEffect)(()=>()=>o({component:null,props:null}),[o]),null}},23555(e,t,n){"use strict";n.d(t,{w:()=>a,J:()=>o});var r=n(30758);const a="navigation-with-keyboard";function o(){(0,r.useEffect)(()=>{function e(e){"keydown"===e.type&&"Tab"===e.key&&document.body.classList.add(a),"mousedown"===e.type&&document.body.classList.remove(a)}return document.addEventListener("keydown",e),document.addEventListener("mousedown",e),()=>{document.body.classList.remove(a),document.removeEventListener("keydown",e),document.removeEventListener("mousedown",e)}},[])}},65029(e,t,n){"use strict";n.d(t,{l:()=>s});var r=n(30758),a=n(11662);const o="desktop",i="mobile";function s({desktopBreakpoint:e=996}={}){const[t,n]=(0,r.useState)(()=>"ssr");return(0,r.useEffect)(()=>{function t(){n(function(e){if(!a.A.canUseDOM)throw new Error("getWindowSize() should only be called after React hydration");return window.innerWidth>e?o:i}(e))}return t(),window.addEventListener("resize",t),()=>{window.removeEventListener("resize",t)}},[e]),t}},46103(e,t,n){"use strict";n.d(t,{G:()=>r});const r={page:{blogListPage:"blog-list-page",blogPostPage:"blog-post-page",blogTagsListPage:"blog-tags-list-page",blogTagPostListPage:"blog-tags-post-list-page",docsDocPage:"docs-doc-page",docsTagsListPage:"docs-tags-list-page",docsTagDocListPage:"docs-tags-doc-list-page",mdxPage:"mdx-page"},wrapper:{main:"main-wrapper",blogPages:"blog-wrapper",docsPages:"docs-wrapper",mdxPages:"mdx-wrapper"},common:{editThisPage:"theme-edit-this-page",lastUpdated:"theme-last-updated",backToTopButton:"theme-back-to-top-button",codeBlock:"theme-code-block",admonition:"theme-admonition",unlistedBanner:"theme-unlisted-banner",admonitionType:e=>`theme-admonition-${e}`},layout:{},docs:{docVersionBanner:"theme-doc-version-banner",docVersionBadge:"theme-doc-version-badge",docBreadcrumbs:"theme-doc-breadcrumbs",docMarkdown:"theme-doc-markdown",docTocMobile:"theme-doc-toc-mobile",docTocDesktop:"theme-doc-toc-desktop",docFooter:"theme-doc-footer",docFooterTagsRow:"theme-doc-footer-tags-row",docFooterEditMetaRow:"theme-doc-footer-edit-meta-row",docSidebarContainer:"theme-doc-sidebar-container",docSidebarMenu:"theme-doc-sidebar-menu",docSidebarItemCategory:"theme-doc-sidebar-item-category",docSidebarItemLink:"theme-doc-sidebar-item-link",docSidebarItemCategoryLevel:e=>`theme-doc-sidebar-item-category-level-${e}`,docSidebarItemLinkLevel:e=>`theme-doc-sidebar-item-link-level-${e}`},blog:{blogFooterTagsRow:"theme-blog-footer-tags-row",blogFooterEditMetaRow:"theme-blog-footer-edit-meta-row"},pages:{pageFooterEditMetaRow:"theme-pages-footer-edit-meta-row"}}},44405(e,t,n){"use strict";function r(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}n.d(t,{O:()=>r})},64878(e,t,n){"use strict";n.d(t,{$S:()=>h,B5:()=>E,C5:()=>p,Nr:()=>g,OF:()=>w,QB:()=>_,Vd:()=>S,Y:()=>v,cC:()=>f,d1:()=>C,fW:()=>k,w8:()=>b});var r=n(30758),a=n(25557),o=n(45757),i=n(72631),s=n(33453),c=n(52092),l=n(66556),u=n(10274),d=n(37505);const p=!!i.Gy;function f(e){const t=(0,c.r)();if(!e)return;const n=t.docs[e];if(!n)throw new Error(`no version doc found by id=${e}`);return n}function g(e){return"link"!==e.type||e.unlisted?"category"===e.type?function(e){if(e.href&&!e.linkUnlisted)return e.href;for(const t of e.items){const e=g(t);if(e)return e}}(e):void 0:e.href}function h(){const{pathname:e}=(0,a.zy)(),t=(0,l.t)();if(!t)throw new Error("Unexpected: cant find current sidebar in context");const n=x({sidebarItems:t.items,pathname:e,onlyCategories:!0}).slice(-1)[0];if(!n)throw new Error(`${e} is not associated with a category. useCurrentSidebarCategory() should only be used on category index pages.`);return n}const m=(e,t)=>void 0!==e&&(0,d.ys)(e,t);function b(e,t){return"link"===e.type?m(e.href,t):"category"===e.type&&(m(e.href,t)||((e,t)=>e.some(e=>b(e,t)))(e.items,t))}function y(e,t){switch(e.type){case"category":return b(e,t)||e.items.some(e=>y(e,t));case"link":return!e.unlisted||b(e,t);default:return!0}}function v(e,t){return(0,r.useMemo)(()=>e.filter(e=>y(e,t)),[e,t])}function x({sidebarItems:e,pathname:t,onlyCategories:n=!1}){const r=[];return function e(a){for(const o of a)if("category"===o.type&&((0,d.ys)(o.href,t)||e(o.items))||"link"===o.type&&(0,d.ys)(o.href,t)){return n&&"category"!==o.type||r.unshift(o),!0}return!1}(e),r}function w(){const e=(0,l.t)(),{pathname:t}=(0,a.zy)(),n=(0,i.vT)()?.pluginData.breadcrumbs;return!1!==n&&e?x({sidebarItems:e.items,pathname:t}):null}function S(e){const{activeVersion:t}=(0,i.zK)(e),{preferredVersion:n}=(0,s.g1)(e),a=(0,i.r7)(e);return(0,r.useMemo)(()=>(0,u.s)([t,n,a].filter(Boolean)),[t,n,a])}function k(e,t){const n=S(t);return(0,r.useMemo)(()=>{const t=n.flatMap(e=>e.sidebars?Object.entries(e.sidebars):[]),r=t.find(t=>t[0]===e);if(!r)throw new Error(`Can't find any sidebar with id "${e}" in version${n.length>1?"s":""} ${n.map(e=>e.name).join(", ")}".\nAvailable sidebar ids are:\n- ${t.map(e=>e[0]).join("\n- ")}`);return r[1]},[e,n])}function _(e,t){const n=S(t);return(0,r.useMemo)(()=>{const t=n.flatMap(e=>e.docs),r=t.find(t=>t.id===e);if(!r){if(n.flatMap(e=>e.draftIds).includes(e))return null;throw new Error(`Couldn't find any doc with id "${e}" in version${n.length>1?"s":""} "${n.map(e=>e.name).join(", ")}".\nAvailable doc ids are:\n- ${(0,u.s)(t.map(e=>e.id)).join("\n- ")}`)}return r},[e,n])}function E({route:e}){const t=(0,a.zy)(),n=(0,c.r)(),r=e.routes,i=r.find(e=>(0,a.B6)(t.pathname,e));if(!i)return null;const s=i.sidebar,l=s?n.docsSidebars[s]:void 0;return{docElement:(0,o.v)(r),sidebarName:s,sidebarItems:l}}function C(e){return e.filter(e=>!("category"===e.type||"link"===e.type)||!!g(e))}},45220(e,t,n){"use strict";n.d(t,{bq:()=>d,MN:()=>u,a2:()=>l,k2:()=>p});var r=n(30758),a=n(61288),o=n(65103);const i="errorBoundaryError_ab8v",s="errorBoundaryFallback_sWpD";var c=n(86070);function l(e){return(0,c.jsx)("button",{type:"button",...e,children:(0,c.jsx)(a.A,{id:"theme.ErrorPageContent.tryAgain",description:"The label of the button to try again rendering when the React error boundary captures an error",children:"Try again"})})}function u({error:e,tryAgain:t}){return(0,c.jsxs)("div",{className:s,children:[(0,c.jsx)("p",{children:e.message}),(0,c.jsx)(l,{onClick:t})]})}function d({error:e}){const t=(0,o.getErrorCausalChain)(e).map(e=>e.message).join("\n\nCause:\n");return(0,c.jsx)("p",{className:i,children:t})}class p extends r.Component{componentDidCatch(e,t){throw this.props.onError(e,t)}render(){return this.props.children}}},64493(e,t,n){"use strict";n.d(t,{$Z:()=>i,aZ:()=>c});var r=n(30758),a=n(25557),o=n(5468);function i(e){!function(e){const t=(0,a.W6)(),n=(0,o._q)(e);(0,r.useEffect)(()=>t.block((e,t)=>n(e,t)),[t,n])}((t,n)=>{if("POP"===n)return e(t,n)})}function s(e){const t=(0,a.W6)();return(0,r.useSyncExternalStore)(t.listen,()=>e(t),()=>e(t))}function c(e){return s(t=>null===e?null:new URLSearchParams(t.location.search).get(e))}},10274(e,t,n){"use strict";function r(e,t=(e,t)=>e===t){return e.filter((n,r)=>e.findIndex(e=>t(e,n))!==r)}function a(e){return Array.from(new Set(e))}n.d(t,{X:()=>r,s:()=>a})},87712(e,t,n){"use strict";n.d(t,{e3:()=>f,be:()=>d,Jx:()=>g});var r=n(30758),a=n(13526),o=n(33959),i=n(65777);function s(){const e=r.useContext(i.o);if(!e)throw new Error("Unexpected: no Docusaurus route context found");return e}var c=n(72646),l=n(90525);var u=n(86070);function d({title:e,description:t,keywords:n,image:r,children:a}){const i=function(e){const{siteConfig:t}=(0,l.A)(),{title:n,titleDelimiter:r}=t;return e?.trim().length?`${e.trim()} ${r} ${n}`:n}(e),{withBaseUrl:s}=(0,c.hH)(),d=r?s(r,{absolute:!0}):void 0;return(0,u.jsxs)(o.A,{children:[e&&(0,u.jsx)("title",{children:i}),e&&(0,u.jsx)("meta",{property:"og:title",content:i}),t&&(0,u.jsx)("meta",{name:"description",content:t}),t&&(0,u.jsx)("meta",{property:"og:description",content:t}),n&&(0,u.jsx)("meta",{name:"keywords",content:Array.isArray(n)?n.join(","):n}),d&&(0,u.jsx)("meta",{property:"og:image",content:d}),d&&(0,u.jsx)("meta",{name:"twitter:image",content:d}),a]})}const p=r.createContext(void 0);function f({className:e,children:t}){const n=r.useContext(p),i=(0,a.A)(n,e);return(0,u.jsxs)(p.Provider,{value:i,children:[(0,u.jsx)(o.A,{children:(0,u.jsx)("html",{className:i})}),t]})}function g({children:e}){const t=s(),n=`plugin-${t.plugin.name.replace(/docusaurus-(?:plugin|theme)-(?:content-)?/gi,"")}`;const r=`plugin-id-${t.plugin.id}`;return(0,u.jsx)(f,{className:(0,a.A)(n,r),children:e})}},5468(e,t,n){"use strict";n.d(t,{Be:()=>l,ZC:()=>s,_q:()=>i,dV:()=>c,fM:()=>u});var r=n(30758),a=n(85924),o=n(86070);function i(e){const t=(0,r.useRef)(e);return(0,a.A)(()=>{t.current=e},[e]),(0,r.useCallback)((...e)=>t.current(...e),[])}function s(e){const t=(0,r.useRef)();return(0,a.A)(()=>{t.current=e}),t.current}class c extends Error{constructor(e,t){super(),this.name="ReactContextError",this.message=`Hook ${this.stack?.split("\n")[1]?.match(/at (?:\w+\.)?(?\w+)/)?.groups.name??""} is called outside the <${e}>. ${t??""}`}}function l(e){const t=Object.entries(e);return t.sort((e,t)=>e[0].localeCompare(t[0])),(0,r.useMemo)(()=>e,t.flat())}function u(e){return({children:t})=>(0,o.jsx)(o.Fragment,{children:e.reduceRight((e,t)=>(0,o.jsx)(t,{children:e}),t)})}},37505(e,t,n){"use strict";n.d(t,{Dt:()=>s,ys:()=>i});var r=n(30758),a=n(81345),o=n(90525);function i(e,t){const n=e=>(!e||e.endsWith("/")?e:`${e}/`)?.toLowerCase();return n(e)===n(t)}function s(){const{baseUrl:e}=(0,o.A)().siteConfig;return(0,r.useMemo)(()=>function({baseUrl:e,routes:t}){function n(t){return t.path===e&&!0===t.exact}function r(t){return t.path===e&&!t.exact}return function e(t){if(0===t.length)return;return t.find(n)||e(t.filter(r).flatMap(e=>e.routes??[]))}(t)}({routes:a.A,baseUrl:e}),[e])}},32416(e,t,n){"use strict";n.d(t,{Mq:()=>f,Tv:()=>u,a_:()=>g,gk:()=>h});var r=n(30758),a=n(11662),o=n(81264),i=n(85924),s=n(5468),c=n(86070);const l=r.createContext(void 0);function u({children:e}){const t=function(){const e=(0,r.useRef)(!0);return(0,r.useMemo)(()=>({scrollEventsEnabledRef:e,enableScrollEvents:()=>{e.current=!0},disableScrollEvents:()=>{e.current=!1}}),[])}();return(0,c.jsx)(l.Provider,{value:t,children:e})}function d(){const e=(0,r.useContext)(l);if(null==e)throw new s.dV("ScrollControllerProvider");return e}const p=()=>a.A.canUseDOM?{scrollX:window.pageXOffset,scrollY:window.pageYOffset}:null;function f(e,t=[]){const{scrollEventsEnabledRef:n}=d(),a=(0,r.useRef)(p()),o=(0,s._q)(e);(0,r.useEffect)(()=>{const e=()=>{if(!n.current)return;const e=p();o(e,a.current),a.current=e},t={passive:!0};return e(),window.addEventListener("scroll",e,t),()=>window.removeEventListener("scroll",e,t)},[o,n,...t])}function g(){const e=d(),t=function(){const e=(0,r.useRef)({elem:null,top:0}),t=(0,r.useCallback)(t=>{e.current={elem:t,top:t.getBoundingClientRect().top}},[]),n=(0,r.useCallback)(()=>{const{current:{elem:t,top:n}}=e;if(!t)return{restored:!1};const r=t.getBoundingClientRect().top-n;return r&&window.scrollBy({left:0,top:r}),e.current={elem:null,top:0},{restored:0!==r}},[]);return(0,r.useMemo)(()=>({save:t,restore:n}),[n,t])}(),n=(0,r.useRef)(void 0),a=(0,r.useCallback)(r=>{t.save(r),e.disableScrollEvents(),n.current=()=>{const{restored:r}=t.restore();if(n.current=void 0,r){const t=()=>{e.enableScrollEvents(),window.removeEventListener("scroll",t)};window.addEventListener("scroll",t)}else e.enableScrollEvents()}},[e,t]);return(0,i.A)(()=>{queueMicrotask(()=>n.current?.())}),{blockElementScrollPositionUntilNextRender:a}}function h(){const e=(0,r.useRef)(null),t=(0,o.A)()&&"smooth"===getComputedStyle(document.documentElement).scrollBehavior;return{startScroll:n=>{e.current=t?function(e){return window.scrollTo({top:e,behavior:"smooth"}),()=>{}}(n):function(e){let t=null;const n=document.documentElement.scrollTop>e;return function r(){const a=document.documentElement.scrollTop;(n&&a>e||!n&&at&&cancelAnimationFrame(t)}(n)},cancelScroll:()=>e.current?.()}}},3703(e,t,n){"use strict";n.d(t,{Cy:()=>r,tU:()=>a});n(90525);const r="default";function a(e,t){return`docs-${e}-${t}`}},44118(e,t,n){"use strict";n.d(t,{Wf:()=>u,Dv:()=>d});var r=n(30758);const a=JSON.parse('{"N":"localStorage","M":""}'),o=a.N;function i({key:e,oldValue:t,newValue:n,storage:r}){if(t===n)return;const a=document.createEvent("StorageEvent");a.initStorageEvent("storage",!1,!1,e,t,n,window.location.href,r),window.dispatchEvent(a)}function s(e=o){if("undefined"==typeof window)throw new Error("Browser storage is not available on Node.js/Docusaurus SSR process.");if("none"===e)return null;try{return window[e]}catch(n){return t=n,c||(console.warn("Docusaurus browser storage is not available.\nPossible reasons: running Docusaurus in an iframe, in an incognito browser session, or using too strict browser privacy settings.",t),c=!0),null}var t}let c=!1;const l={get:()=>null,set:()=>{},del:()=>{},listen:()=>()=>{}};function u(e,t){const n=`${e}${a.M}`;if("undefined"==typeof window)return function(e){function t(){throw new Error(`Illegal storage API usage for storage key "${e}".\nDocusaurus storage APIs are not supposed to be called on the server-rendering process.\nPlease only call storage APIs in effects and event handlers.`)}return{get:t,set:t,del:t,listen:t}}(n);const r=s(t?.persistence);return null===r?l:{get:()=>{try{return r.getItem(n)}catch(e){return console.error(`Docusaurus storage error, can't get key=${n}`,e),null}},set:e=>{try{const t=r.getItem(n);r.setItem(n,e),i({key:n,oldValue:t,newValue:e,storage:r})}catch(t){console.error(`Docusaurus storage error, can't set ${n}=${e}`,t)}},del:()=>{try{const e=r.getItem(n);r.removeItem(n),i({key:n,oldValue:e,newValue:null,storage:r})}catch(e){console.error(`Docusaurus storage error, can't delete key=${n}`,e)}},listen:e=>{try{const t=t=>{t.storageArea===r&&t.key===n&&e(t)};return window.addEventListener("storage",t),()=>window.removeEventListener("storage",t)}catch(t){return console.error(`Docusaurus storage error, can't listen for changes of key=${n}`,t),()=>{}}}}}function d(e,t){const n=(0,r.useRef)(()=>null===e?l:u(e,t)).current(),a=(0,r.useCallback)(e=>"undefined"==typeof window?()=>{}:n.listen(e),[n]);return[(0,r.useSyncExternalStore)(a,()=>"undefined"==typeof window?null:n.get(),()=>null),n]}},99555(e,t,n){"use strict";n.d(t,{o:()=>i});var r=n(90525),a=n(25557),o=n(65103);function i(){const{siteConfig:{baseUrl:e,url:t,trailingSlash:n},i18n:{defaultLocale:i,currentLocale:s}}=(0,r.A)(),{pathname:c}=(0,a.zy)(),l=(0,o.applyTrailingSlash)(c,{trailingSlash:n,baseUrl:e}),u=s===i?e:e.replace(`/${s}/`,"/"),d=l.replace(e,"");return{createUrl:function({locale:e,fullyQualified:n}){return`${n?t:""}${function(e){return e===i?`${u}`:`${u}${e}/`}(e)}${d}`}}}},52438(e,t,n){"use strict";n.d(t,{$:()=>i});var r=n(30758),a=n(25557),o=n(5468);function i(e){const t=(0,a.zy)(),n=(0,o.ZC)(t),i=(0,o._q)(e);(0,r.useEffect)(()=>{n&&t!==n&&i({location:t,previousLocation:n})},[i,t,n])}},91878(e,t,n){"use strict";n.d(t,{p:()=>a});var r=n(90525);function a(){return(0,r.A)().siteConfig.themeConfig}},79614(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.removeTrailingSlash=t.addLeadingSlash=t.addTrailingSlash=void 0;const r=n(55313);function a(e){return e.endsWith("/")?e:`${e}/`}function o(e){return(0,r.removeSuffix)(e,"/")}t.addTrailingSlash=a,t.default=function(e,t){const{trailingSlash:n,baseUrl:r}=t;if(e.startsWith("#"))return e;if(void 0===n)return e;const[i]=e.split(/[#?]/),s="/"===i||i===r?i:(c=i,n?a(c):o(c));var c;return e.replace(i,s)},t.addLeadingSlash=function(e){return(0,r.addPrefix)(e,"/")},t.removeTrailingSlash=o},14292(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getErrorCausalChain=void 0,t.getErrorCausalChain=function e(t){return t.cause?[t,...e(t.cause)]:[t]}},65103(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.getErrorCausalChain=t.removePrefix=t.addSuffix=t.removeSuffix=t.addPrefix=t.removeTrailingSlash=t.addLeadingSlash=t.addTrailingSlash=t.applyTrailingSlash=t.blogPostContainerID=void 0,t.blogPostContainerID="__blog-post-container";var a=n(79614);Object.defineProperty(t,"applyTrailingSlash",{enumerable:!0,get:function(){return r(a).default}}),Object.defineProperty(t,"addTrailingSlash",{enumerable:!0,get:function(){return a.addTrailingSlash}}),Object.defineProperty(t,"addLeadingSlash",{enumerable:!0,get:function(){return a.addLeadingSlash}}),Object.defineProperty(t,"removeTrailingSlash",{enumerable:!0,get:function(){return a.removeTrailingSlash}});var o=n(55313);Object.defineProperty(t,"addPrefix",{enumerable:!0,get:function(){return o.addPrefix}}),Object.defineProperty(t,"removeSuffix",{enumerable:!0,get:function(){return o.removeSuffix}}),Object.defineProperty(t,"addSuffix",{enumerable:!0,get:function(){return o.addSuffix}}),Object.defineProperty(t,"removePrefix",{enumerable:!0,get:function(){return o.removePrefix}});var i=n(14292);Object.defineProperty(t,"getErrorCausalChain",{enumerable:!0,get:function(){return i.getErrorCausalChain}})},55313(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.removePrefix=t.addSuffix=t.removeSuffix=t.addPrefix=void 0,t.addPrefix=function(e,t){return e.startsWith(t)?e:`${t}${e}`},t.removeSuffix=function(e,t){return""===t?e:e.endsWith(t)?e.slice(0,-t.length):e},t.addSuffix=function(e,t){return e.endsWith(t)?e:`${e}${t}`},t.removePrefix=function(e,t){return e.startsWith(t)?e.slice(t.length):e}},93939(e,t,n){"use strict";n.d(t,{A:()=>i});n(30758);var r=n(68835);const a="loadingRing_U86y";var o=n(86070);function i({className:e}){return(0,o.jsxs)("div",{className:(0,r.A)(a,e),children:[(0,o.jsx)("div",{}),(0,o.jsx)("div",{}),(0,o.jsx)("div",{}),(0,o.jsx)("div",{})]})}},16451(e,t,n){"use strict";n.d(t,{Z:()=>s});var r=n(36430),a=n.n(r),o=n(79905);const i=new Map;function s(e,t){const n=`${e}${t}`;let r=i.get(n);return r||(r=async function(e,t){{const n=`${e}${o.IH.replace("{dir}",t?`-${t.replace(/\//g,"-")}`:"")}`;if(new URL(n,location.origin).origin!==location.origin)throw new Error("Unexpected version url");const r=await(await fetch(n)).json(),i=r.map(({documents:e,index:t},n)=>({type:n,documents:e,index:a().Index.load(t)})),s=r.reduce((e,t)=>{for(const n of t.index.invertedIndex)/\p{Unified_Ideograph}/u.test(n[0][0])&&e.add(n[0]);return e},new Set);return{wrappedIndexes:i,zhDictionary:Array.from(s)}}}(e,t),i.set(n,r)),r}},68981(e,t,n){"use strict";n.d(t,{m:()=>l});var r=n(36430),a=n.n(r);var o=n(79905);function i(e){return s(e).concat(s(e.filter(e=>{const t=e[e.length-1];return!t.trailing&&t.maybeTyping}),!0))}function s(e,t){return e.map(e=>({tokens:e.map(e=>e.value),term:e.map(e=>({value:e.value,presence:a().Query.presence.REQUIRED,wildcard:(t?e.trailing||e.maybeTyping:e.trailing)?a().Query.wildcard.TRAILING:a().Query.wildcard.NONE}))}))}var c=n(37753);function l(e,t,n){return function(r,s){const l=function(e,t){if(1===t.length&&["ja","jp","th"].includes(t[0]))return a()[t[0]].tokenizer(e).map(e=>e.toString());let n=/[^-\s]+/g;return t.includes("zh")&&(n=/\w+|\p{Unified_Ideograph}+/gu),e.toLowerCase().match(n)||[]}(r,o.BH);if(0===l.length)return void s([]);const u=function(e,t){const n=function(e,t){const n=[];return function e(r,a){if(0===r.length)return void n.push(a);const o=r[0];if(/\p{Unified_Ideograph}/u.test(o)){const n=function(e,t){const n=[];return function e(r,a){let o=0,i=!1;for(const s of t)if(r.substr(0,s.length)===s){const t={missed:a.missed,term:a.term.concat({value:s})};r.length>s.length?e(r.substr(s.length),t):n.push(t),i=!0}else for(let t=s.length-1;t>o;t-=1){const c=s.substr(0,t);if(r.substr(0,t)===c){o=t;const s={missed:a.missed,term:a.term.concat({value:c,trailing:!0})};r.length>t?e(r.substr(t),s):n.push(s),i=!0;break}}i||(r.length>0?e(r.substr(1),{missed:a.missed+1,term:a.term}):a.term.length>0&&n.push(a))}(e,{missed:0,term:[]}),n.sort((e,t)=>{const n=e.missed>0?1:0,r=t.missed>0?1:0;return n!==r?n-r:e.term.length-t.term.length}).map(e=>e.term)}(o,t);for(const t of n){const n=a.concat(...t);e(r.slice(1),n)}}else{const t=a.concat({value:o});e(r.slice(1),t)}}(e,[]),n}(e,t);if(0===n.length)return[{tokens:e,term:e.map(e=>({value:e,presence:a().Query.presence.REQUIRED,wildcard:a().Query.wildcard.LEADING|a().Query.wildcard.TRAILING}))}];for(const a of n)a[a.length-1].maybeTyping=!0;const r=[];for(const i of o.BH)if("en"===i)o.sx||r.unshift(a().stopWordFilter);else{const e=a()[i];e.stopWordFilter&&r.unshift(e.stopWordFilter)}let s;if(r.length>0){const e=e=>r.reduce((e,t)=>e.filter(e=>t(e.value)),e);s=[];const t=[];for(const r of n){const n=e(r);s.push(n),n.length0&&t.push(n)}n.push(...t)}else s=n.slice();const c=[];for(const a of s)if(a.length>2)for(let e=a.length-1;e>=0;e-=1)c.push(a.slice(0,e).concat(a.slice(e+1)));return i(n).concat(i(c))}(l,t),d=[];e:for(const{term:t,tokens:a}of u)for(const{documents:r,index:o,type:i}of e)if(d.push(...o.query(e=>{for(const n of t)e.term(n.value,{wildcard:n.wildcard,presence:n.presence})}).slice(0,n).filter(e=>!d.some(t=>t.document.i.toString()===e.ref)).slice(0,n-d.length).map(t=>{const n=r.find(e=>e.i.toString()===t.ref);return{document:n,type:i,page:i!==c.i.Title&&e[0].documents.find(e=>e.i===n.p),metadata:t.matchData.metadata,tokens:a,score:t.score}})),d.length>=n)break e;!function(e){e.forEach((e,t)=>{e.index=t}),e.sort((t,n)=>{let r=t.type!==c.i.Heading&&t.type!==c.i.Content&&t.type!==c.i.Description||!t.page?t.index:e.findIndex(e=>e.document===t.page),a=n.type!==c.i.Heading&&n.type!==c.i.Content&&n.type!==c.i.Description||!n.page?n.index:e.findIndex(e=>e.document===n.page);if(-1===r&&(r=t.index),-1===a&&(a=n.index),r===a){const e=(0===n.type?1:0)-(0===t.type?1:0);return 0===e?t.index-n.index:e}return r-a})}(d),function(e){e.forEach((t,n)=>{n>0&&t.page&&e.slice(0,n).some(e=>(e.type===c.i.Keywords?e.page:e.document)===t.page)&&(nr})},28751(e,t,n){"use strict";function r(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}n.d(t,{Z:()=>r})},24442(e,t,n){"use strict";function r(e,t){const n=[];for(const r of Object.values(e))r[t]&&n.push(...r[t].position);return n.sort((e,t)=>e[0]-t[0]||t[1]-e[1])}n.d(t,{g:()=>r})},72313(e,t,n){"use strict";n.d(t,{Z:()=>a});var r=n(28751);function a(e,t,n){const o=[];for(const i of t){const n=e.toLowerCase().indexOf(i);if(n>=0){n>0&&o.push(a(e.substr(0,n),t)),o.push(`${(0,r.Z)(e.substr(n,i.length))}`);const s=n+i.length;s${(0,r.Z)(e)}`:(0,r.Z)(e):o.join("")}},70406(e,t,n){"use strict";n.d(t,{C:()=>c});var r=n(28751),a=n(72313);const o=/\w+|\p{Unified_Ideograph}/u;function i(e){const t=[];let n=0,r=e;for(;r.length>0;){const a=r.match(o);if(!a){t.push(r);break}a.index>0&&t.push(r.substring(0,a.index)),t.push(a[0]),n+=a.index+a[0].length,r=e.substring(n)}return t}var s=n(79905);function c(e,t,n,o=s.rG){const{chunkIndex:c,chunks:l}=function(e,t,n){const o=[];let s=0,c=0,l=-1;for(;sc){const t=i(e.substring(c,u)).map(e=>({html:(0,r.Z)(e),textLength:e.length}));for(const e of t)o.push(e)}-1===l&&(l=o.length),c=u+d,o.push({html:(0,a.Z)(e.substring(u,c),n,!0),textLength:d})}}if(c({html:(0,r.Z)(e),textLength:e.length}));for(const e of t)o.push(e)}return{chunkIndex:l,chunks:o}}(e,t,n),u=l.slice(0,c),d=l[c],p=[d.html],f=l.slice(c+1);let g=d.textLength,h=0,m=0,b=!1,y=!1;for(;g0){const e=u.pop();g+e.textLength<=o?(p.unshift(e.html),h+=e.textLength,g+=e.textLength):(b=!0,u.length=0)}else{if(!(f.length>0))break;{const e=f.shift();g+e.textLength<=o?(p.push(e.html),m+=e.textLength,g+=e.textLength):(y=!0,f.length=0)}}return(b||u.length>0)&&p.unshift("\u2026"),(y||f.length>0)&&p.push("\u2026"),p.join("")}},57433(e,t,n){"use strict";function r(e,t){if("string"==typeof e)return{label:e,path:e};{const{label:n,path:r}=e;return"string"==typeof n?{label:n,path:r}:Object.prototype.hasOwnProperty.call(n,t)?{label:n[t],path:r}:{label:r,path:r}}}n.d(t,{p:()=>r})},79905(e,t,n){"use strict";n.d(t,{CU:()=>a(),UB:()=>g,tb:()=>u,O6:()=>b,I$:()=>h,BH:()=>o,sx:()=>i,ZG:()=>f,WW:()=>d,pk:()=>p,Hg:()=>m,IH:()=>s,rG:()=>l,AT:()=>c,dz:()=>y});n(36430);var r=n(91176),a=n.n(r);const o=["en"],i=!1,s="search-index{dir}.json?_=6fe05f60",c=8,l=50,u=!0,d=!0,p=!0,f="right",g=void 0,h=!0,m=null,b=!1,y=!1},37753(e,t,n){"use strict";var r;n.d(t,{i:()=>r}),function(e){e[e.Title=0]="Title",e[e.Heading=1]="Heading",e[e.Description=2]="Description",e[e.Keywords=3]="Keywords",e[e.Content=4]="Content"}(r||(r={}))},17101(e,t,n){"use strict";n.d(t,{A:()=>f});n(30758);var r=n(46753),a=n(72646);const o="custom-footer-wrapper_fHnE",i="logo-wrapper_GEfd",s="dark-theme-logo_tdev",c="light-theme-logo_cRqu",l="copyright_aTku",u="footerSocialIconsWrapper_tJhP",d="socialBrands__tjK";var p=n(86070);const f=()=>(0,p.jsxs)("footer",{className:o,children:[(0,p.jsxs)("div",{className:i,children:[(0,p.jsx)("img",{src:(0,a.Ay)("/img/rcommon-logo-light-bg.png"),className:c,alt:"RCommon"}),(0,p.jsx)("img",{src:(0,a.Ay)("/img/rcommon-logo-dark-bg.png"),className:s,alt:"RCommon"})]}),(0,p.jsx)("div",{className:l,children:`\xa9 ${(new Date).getFullYear()} RCommon Team. All rights reserved`}),(0,p.jsxs)("div",{className:u,children:[(0,p.jsx)("div",{className:d,children:(0,p.jsx)(r.A,{href:"https://github.com/RCommon-Team/RCommon",rel:"noopener noreferrer","aria-label":"GitHub",children:(0,p.jsx)("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"currentColor",xmlns:"http://www.w3.org/2000/svg",children:(0,p.jsx)("path",{d:"M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.166 6.839 9.489.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.604-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.464-1.11-1.464-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836a9.59 9.59 0 012.504.337c1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.161 22 16.416 22 12c0-5.523-4.477-10-10-10z"})})})}),(0,p.jsx)("div",{className:d,children:(0,p.jsx)(r.A,{href:"https://www.nuget.org/profiles/RCommon",rel:"noopener noreferrer","aria-label":"NuGet",children:(0,p.jsxs)("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"currentColor",xmlns:"http://www.w3.org/2000/svg",children:[(0,p.jsx)("circle",{cx:"7",cy:"17",r:"3.5"}),(0,p.jsx)("circle",{cx:"16",cy:"11",r:"5"}),(0,p.jsx)("circle",{cx:"7.5",cy:"7.5",r:"2"})]})})}),(0,p.jsx)("div",{className:d,children:(0,p.jsx)(r.A,{href:"https://twitter.com/RCommonTeam",rel:"noopener noreferrer","aria-label":"Twitter / X",children:(0,p.jsx)("svg",{width:"24",height:"24",viewBox:"0 0 24 24",fill:"currentColor",xmlns:"http://www.w3.org/2000/svg",children:(0,p.jsx)("path",{d:"M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"})})})})]})]})},68835(e,t,n){"use strict";function r(e){var t,n,a="";if("string"==typeof e||"number"==typeof e)a+=e;else if("object"==typeof e)if(Array.isArray(e))for(t=0;ta});const a=function(){for(var e,t,n=0,a="";nx,TM:()=>C,yJ:()=>f,sC:()=>A,AO:()=>p});var r=n(78621);function a(e){return"/"===e.charAt(0)}function o(e,t){for(var n=t,r=n+1,a=e.length;r=0;p--){var f=i[p];"."===f?o(i,p):".."===f?(o(i,p),d++):d&&(o(i,p),d--)}if(!l)for(;d--;d)i.unshift("..");!l||""===i[0]||i[0]&&a(i[0])||i.unshift("");var g=i.join("/");return n&&"/"!==g.substr(-1)&&(g+="/"),g};var s=n(25385);function c(e){return"/"===e.charAt(0)?e:"/"+e}function l(e){return"/"===e.charAt(0)?e.substr(1):e}function u(e,t){return function(e,t){return 0===e.toLowerCase().indexOf(t.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(t.length))}(e,t)?e.substr(t.length):e}function d(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}function p(e){var t=e.pathname,n=e.search,r=e.hash,a=t||"/";return n&&"?"!==n&&(a+="?"===n.charAt(0)?n:"?"+n),r&&"#"!==r&&(a+="#"===r.charAt(0)?r:"#"+r),a}function f(e,t,n,a){var o;"string"==typeof e?(o=function(e){var t=e||"/",n="",r="",a=t.indexOf("#");-1!==a&&(r=t.substr(a),t=t.substr(0,a));var o=t.indexOf("?");return-1!==o&&(n=t.substr(o),t=t.substr(0,o)),{pathname:t,search:"?"===n?"":n,hash:"#"===r?"":r}}(e),o.state=t):(void 0===(o=(0,r.A)({},e)).pathname&&(o.pathname=""),o.search?"?"!==o.search.charAt(0)&&(o.search="?"+o.search):o.search="",o.hash?"#"!==o.hash.charAt(0)&&(o.hash="#"+o.hash):o.hash="",void 0!==t&&void 0===o.state&&(o.state=t));try{o.pathname=decodeURI(o.pathname)}catch(s){throw s instanceof URIError?new URIError('Pathname "'+o.pathname+'" could not be decoded. This is likely caused by an invalid percent-encoding.'):s}return n&&(o.key=n),a?o.pathname?"/"!==o.pathname.charAt(0)&&(o.pathname=i(o.pathname,a.pathname)):o.pathname=a.pathname:o.pathname||(o.pathname="/"),o}function g(){var e=null;var t=[];return{setPrompt:function(t){return e=t,function(){e===t&&(e=null)}},confirmTransitionTo:function(t,n,r,a){if(null!=e){var o="function"==typeof e?e(t,n):e;"string"==typeof o?"function"==typeof r?r(o,a):a(!0):a(!1!==o)}else a(!0)},appendListener:function(e){var n=!0;function r(){n&&e.apply(void 0,arguments)}return t.push(r),function(){n=!1,t=t.filter(function(e){return e!==r})}},notifyListeners:function(){for(var e=arguments.length,n=new Array(e),r=0;rt?n.splice(t,n.length-t,a):n.push(a),d({action:r,location:a,index:t,entries:n})}})},replace:function(e,t){var r="REPLACE",a=f(e,t,h(),x.location);u.confirmTransitionTo(a,r,n,function(e){e&&(x.entries[x.index]=a,d({action:r,location:a}))})},go:v,goBack:function(){v(-1)},goForward:function(){v(1)},canGo:function(e){var t=x.index+e;return t>=0&&t0){var c=I.utils.clone(t)||{};c.position=[i,s],c.index=a.length,a.push(new I.Token(n.slice(i,o),c))}i=o+1}}return a},I.tokenizer.separator=/[\s\-]+/,I.Pipeline=function(){this._stack=[]},I.Pipeline.registeredFunctions=Object.create(null),I.Pipeline.registerFunction=function(e,t){t in this.registeredFunctions&&I.utils.warn("Overwriting existing registered function: "+t),e.label=t,I.Pipeline.registeredFunctions[e.label]=e},I.Pipeline.warnIfFunctionNotRegistered=function(e){e.label&&e.label in this.registeredFunctions||I.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},I.Pipeline.load=function(e){var t=new I.Pipeline;return e.forEach(function(e){var n=I.Pipeline.registeredFunctions[e];if(!n)throw new Error("Cannot load unregistered function: "+e);t.add(n)}),t},I.Pipeline.prototype.add=function(){Array.prototype.slice.call(arguments).forEach(function(e){I.Pipeline.warnIfFunctionNotRegistered(e),this._stack.push(e)},this)},I.Pipeline.prototype.after=function(e,t){I.Pipeline.warnIfFunctionNotRegistered(t);var n=this._stack.indexOf(e);if(-1==n)throw new Error("Cannot find existingFn");n+=1,this._stack.splice(n,0,t)},I.Pipeline.prototype.before=function(e,t){I.Pipeline.warnIfFunctionNotRegistered(t);var n=this._stack.indexOf(e);if(-1==n)throw new Error("Cannot find existingFn");this._stack.splice(n,0,t)},I.Pipeline.prototype.remove=function(e){var t=this._stack.indexOf(e);-1!=t&&this._stack.splice(t,1)},I.Pipeline.prototype.run=function(e){for(var t=this._stack.length,n=0;n1&&(oe&&(n=a),o!=e);)r=n-t,a=t+Math.floor(r/2),o=this.elements[2*a];return o==e||o>e?2*a:os?l+=2:i==s&&(t+=n[c+1]*r[l+1],c+=2,l+=2);return t},I.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},I.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),t=1,n=0;t0){var o,i=a.str.charAt(0);i in a.node.edges?o=a.node.edges[i]:(o=new I.TokenSet,a.node.edges[i]=o),1==a.str.length&&(o.final=!0),r.push({node:o,editsRemaining:a.editsRemaining,str:a.str.slice(1)})}if(0!=a.editsRemaining){if("*"in a.node.edges)var s=a.node.edges["*"];else{s=new I.TokenSet;a.node.edges["*"]=s}if(0==a.str.length&&(s.final=!0),r.push({node:s,editsRemaining:a.editsRemaining-1,str:a.str}),a.str.length>1&&r.push({node:a.node,editsRemaining:a.editsRemaining-1,str:a.str.slice(1)}),1==a.str.length&&(a.node.final=!0),a.str.length>=1){if("*"in a.node.edges)var c=a.node.edges["*"];else{c=new I.TokenSet;a.node.edges["*"]=c}1==a.str.length&&(c.final=!0),r.push({node:c,editsRemaining:a.editsRemaining-1,str:a.str.slice(1)})}if(a.str.length>1){var l,u=a.str.charAt(0),d=a.str.charAt(1);d in a.node.edges?l=a.node.edges[d]:(l=new I.TokenSet,a.node.edges[d]=l),1==a.str.length&&(l.final=!0),r.push({node:l,editsRemaining:a.editsRemaining-1,str:u+a.str.slice(2)})}}}return n},I.TokenSet.fromString=function(e){for(var t=new I.TokenSet,n=t,r=0,a=e.length;r=e;t--){var n=this.uncheckedNodes[t],r=n.child.toString();r in this.minimizedNodes?n.parent.edges[n.char]=this.minimizedNodes[r]:(n.child._str=r,this.minimizedNodes[r]=n.child),this.uncheckedNodes.pop()}},I.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},I.Index.prototype.search=function(e){return this.query(function(t){new I.QueryParser(e,t).parse()})},I.Index.prototype.query=function(e){for(var t=new I.Query(this.fields),n=Object.create(null),r=Object.create(null),a=Object.create(null),o=Object.create(null),i=Object.create(null),s=0;s1?1:e},I.Builder.prototype.k1=function(e){this._k1=e},I.Builder.prototype.add=function(e,t){var n=e[this._ref],r=Object.keys(this._fields);this._documents[n]=t||{},this.documentCount+=1;for(var a=0;a=this.length)return I.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},I.QueryLexer.prototype.width=function(){return this.pos-this.start},I.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},I.QueryLexer.prototype.backup=function(){this.pos-=1},I.QueryLexer.prototype.acceptDigitRun=function(){var e,t;do{t=(e=this.next()).charCodeAt(0)}while(t>47&&t<58);e!=I.QueryLexer.EOS&&this.backup()},I.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(I.QueryLexer.TERM)),e.ignore(),e.more())return I.QueryLexer.lexText},I.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(I.QueryLexer.EDIT_DISTANCE),I.QueryLexer.lexText},I.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(I.QueryLexer.BOOST),I.QueryLexer.lexText},I.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(I.QueryLexer.TERM)},I.QueryLexer.termSeparator=I.tokenizer.separator,I.QueryLexer.lexText=function(e){for(;;){var t=e.next();if(t==I.QueryLexer.EOS)return I.QueryLexer.lexEOS;if(92!=t.charCodeAt(0)){if(":"==t)return I.QueryLexer.lexField;if("~"==t)return e.backup(),e.width()>0&&e.emit(I.QueryLexer.TERM),I.QueryLexer.lexEditDistance;if("^"==t)return e.backup(),e.width()>0&&e.emit(I.QueryLexer.TERM),I.QueryLexer.lexBoost;if("+"==t&&1===e.width())return e.emit(I.QueryLexer.PRESENCE),I.QueryLexer.lexText;if("-"==t&&1===e.width())return e.emit(I.QueryLexer.PRESENCE),I.QueryLexer.lexText;if(t.match(I.QueryLexer.termSeparator))return I.QueryLexer.lexTerm}else e.escapeCharacter()}},I.QueryParser=function(e,t){this.lexer=new I.QueryLexer(e),this.query=t,this.currentClause={},this.lexemeIdx=0},I.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=I.QueryParser.parseClause;e;)e=e(this);return this.query},I.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},I.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},I.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},I.QueryParser.parseClause=function(e){var t=e.peekLexeme();if(null!=t)switch(t.type){case I.QueryLexer.PRESENCE:return I.QueryParser.parsePresence;case I.QueryLexer.FIELD:return I.QueryParser.parseField;case I.QueryLexer.TERM:return I.QueryParser.parseTerm;default:var n="expected either a field or a term, found "+t.type;throw t.str.length>=1&&(n+=" with value '"+t.str+"'"),new I.QueryParseError(n,t.start,t.end)}},I.QueryParser.parsePresence=function(e){var t=e.consumeLexeme();if(null!=t){switch(t.str){case"-":e.currentClause.presence=I.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=I.Query.presence.REQUIRED;break;default:var n="unrecognised presence operator'"+t.str+"'";throw new I.QueryParseError(n,t.start,t.end)}var r=e.peekLexeme();if(null==r){n="expecting term or field, found nothing";throw new I.QueryParseError(n,t.start,t.end)}switch(r.type){case I.QueryLexer.FIELD:return I.QueryParser.parseField;case I.QueryLexer.TERM:return I.QueryParser.parseTerm;default:n="expecting term or field, found '"+r.type+"'";throw new I.QueryParseError(n,r.start,r.end)}}},I.QueryParser.parseField=function(e){var t=e.consumeLexeme();if(null!=t){if(-1==e.query.allFields.indexOf(t.str)){var n=e.query.allFields.map(function(e){return"'"+e+"'"}).join(", "),r="unrecognised field '"+t.str+"', possible fields: "+n;throw new I.QueryParseError(r,t.start,t.end)}e.currentClause.fields=[t.str];var a=e.peekLexeme();if(null==a){r="expecting term, found nothing";throw new I.QueryParseError(r,t.start,t.end)}if(a.type===I.QueryLexer.TERM)return I.QueryParser.parseTerm;r="expecting term, found '"+a.type+"'";throw new I.QueryParseError(r,a.start,a.end)}},I.QueryParser.parseTerm=function(e){var t=e.consumeLexeme();if(null!=t){e.currentClause.term=t.str.toLowerCase(),-1!=t.str.indexOf("*")&&(e.currentClause.usePipeline=!1);var n=e.peekLexeme();if(null!=n)switch(n.type){case I.QueryLexer.TERM:return e.nextClause(),I.QueryParser.parseTerm;case I.QueryLexer.FIELD:return e.nextClause(),I.QueryParser.parseField;case I.QueryLexer.EDIT_DISTANCE:return I.QueryParser.parseEditDistance;case I.QueryLexer.BOOST:return I.QueryParser.parseBoost;case I.QueryLexer.PRESENCE:return e.nextClause(),I.QueryParser.parsePresence;default:var r="Unexpected lexeme type '"+n.type+"'";throw new I.QueryParseError(r,n.start,n.end)}else e.nextClause()}},I.QueryParser.parseEditDistance=function(e){var t=e.consumeLexeme();if(null!=t){var n=parseInt(t.str,10);if(isNaN(n)){var r="edit distance must be numeric";throw new I.QueryParseError(r,t.start,t.end)}e.currentClause.editDistance=n;var a=e.peekLexeme();if(null!=a)switch(a.type){case I.QueryLexer.TERM:return e.nextClause(),I.QueryParser.parseTerm;case I.QueryLexer.FIELD:return e.nextClause(),I.QueryParser.parseField;case I.QueryLexer.EDIT_DISTANCE:return I.QueryParser.parseEditDistance;case I.QueryLexer.BOOST:return I.QueryParser.parseBoost;case I.QueryLexer.PRESENCE:return e.nextClause(),I.QueryParser.parsePresence;default:r="Unexpected lexeme type '"+a.type+"'";throw new I.QueryParseError(r,a.start,a.end)}else e.nextClause()}},I.QueryParser.parseBoost=function(e){var t=e.consumeLexeme();if(null!=t){var n=parseInt(t.str,10);if(isNaN(n)){var r="boost must be numeric";throw new I.QueryParseError(r,t.start,t.end)}e.currentClause.boost=n;var a=e.peekLexeme();if(null!=a)switch(a.type){case I.QueryLexer.TERM:return e.nextClause(),I.QueryParser.parseTerm;case I.QueryLexer.FIELD:return e.nextClause(),I.QueryParser.parseField;case I.QueryLexer.EDIT_DISTANCE:return I.QueryParser.parseEditDistance;case I.QueryLexer.BOOST:return I.QueryParser.parseBoost;case I.QueryLexer.PRESENCE:return e.nextClause(),I.QueryParser.parsePresence;default:r="Unexpected lexeme type '"+a.type+"'";throw new I.QueryParseError(r,a.start,a.end)}else e.nextClause()}},void 0===(a="function"==typeof(r=function(){return I})?r.call(t,n,t,e):r)||(e.exports=a)}()},91176(e){e.exports=function(){"use strict";var e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},n=function(){function e(e,t){for(var n=0;n1&&void 0!==arguments[1])||arguments[1],a=arguments.length>2&&void 0!==arguments[2]?arguments[2]:[],o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:5e3;t(this,e),this.ctx=n,this.iframes=r,this.exclude=a,this.iframesTimeout=o}return n(e,[{key:"getContexts",value:function(){var e=[];return(void 0!==this.ctx&&this.ctx?NodeList.prototype.isPrototypeOf(this.ctx)?Array.prototype.slice.call(this.ctx):Array.isArray(this.ctx)?this.ctx:"string"==typeof this.ctx?Array.prototype.slice.call(document.querySelectorAll(this.ctx)):[this.ctx]:[]).forEach(function(t){var n=e.filter(function(e){return e.contains(t)}).length>0;-1!==e.indexOf(t)||n||e.push(t)}),e}},{key:"getIframeContents",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){},r=void 0;try{var a=e.contentWindow;if(r=a.document,!a||!r)throw new Error("iframe inaccessible")}catch(o){n()}r&&t(r)}},{key:"isIframeBlank",value:function(e){var t="about:blank",n=e.getAttribute("src").trim();return e.contentWindow.location.href===t&&n!==t&&n}},{key:"observeIframeLoad",value:function(e,t,n){var r=this,a=!1,o=null,i=function i(){if(!a){a=!0,clearTimeout(o);try{r.isIframeBlank(e)||(e.removeEventListener("load",i),r.getIframeContents(e,t,n))}catch(s){n()}}};e.addEventListener("load",i),o=setTimeout(i,this.iframesTimeout)}},{key:"onIframeReady",value:function(e,t,n){try{"complete"===e.contentWindow.document.readyState?this.isIframeBlank(e)?this.observeIframeLoad(e,t,n):this.getIframeContents(e,t,n):this.observeIframeLoad(e,t,n)}catch(r){n()}}},{key:"waitForIframes",value:function(e,t){var n=this,r=0;this.forEachIframe(e,function(){return!0},function(e){r++,n.waitForIframes(e.querySelector("html"),function(){--r||t()})},function(e){e||t()})}},{key:"forEachIframe",value:function(t,n,r){var a=this,o=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},i=t.querySelectorAll("iframe"),s=i.length,c=0;i=Array.prototype.slice.call(i);var l=function(){--s<=0&&o(c)};s||l(),i.forEach(function(t){e.matches(t,a.exclude)?l():a.onIframeReady(t,function(e){n(t)&&(c++,r(e)),l()},l)})}},{key:"createIterator",value:function(e,t,n){return document.createNodeIterator(e,t,n,!1)}},{key:"createInstanceOnIframe",value:function(t){return new e(t.querySelector("html"),this.iframes)}},{key:"compareNodeIframe",value:function(e,t,n){if(e.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_PRECEDING){if(null===t)return!0;if(t.compareDocumentPosition(n)&Node.DOCUMENT_POSITION_FOLLOWING)return!0}return!1}},{key:"getIteratorNode",value:function(e){var t=e.previousNode();return{prevNode:t,node:(null===t||e.nextNode())&&e.nextNode()}}},{key:"checkIframeFilter",value:function(e,t,n,r){var a=!1,o=!1;return r.forEach(function(e,t){e.val===n&&(a=t,o=e.handled)}),this.compareNodeIframe(e,t,n)?(!1!==a||o?!1===a||o||(r[a].handled=!0):r.push({val:n,handled:!0}),!0):(!1===a&&r.push({val:n,handled:!1}),!1)}},{key:"handleOpenIframes",value:function(e,t,n,r){var a=this;e.forEach(function(e){e.handled||a.getIframeContents(e.val,function(e){a.createInstanceOnIframe(e).forEachNode(t,n,r)})})}},{key:"iterateThroughNodes",value:function(e,t,n,r,a){for(var o=this,i=this.createIterator(t,e,r),s=[],c=[],l=void 0,u=void 0,d=function(){var e=o.getIteratorNode(i);return u=e.prevNode,l=e.node};d();)this.iframes&&this.forEachIframe(t,function(e){return o.checkIframeFilter(l,u,e,s)},function(t){o.createInstanceOnIframe(t).forEachNode(e,function(e){return c.push(e)},r)}),c.push(l);c.forEach(function(e){n(e)}),this.iframes&&this.handleOpenIframes(s,e,n,r),a()}},{key:"forEachNode",value:function(e,t,n){var r=this,a=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){},o=this.getContexts(),i=o.length;i||a(),o.forEach(function(o){var s=function(){r.iterateThroughNodes(e,o,t,n,function(){--i<=0&&a()})};r.iframes?r.waitForIframes(o,s):s()})}}],[{key:"matches",value:function(e,t){var n="string"==typeof t?[t]:t,r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.oMatchesSelector||e.webkitMatchesSelector;if(r){var a=!1;return n.every(function(t){return!r.call(e,t)||(a=!0,!1)}),a}return!1}}]),e}(),o=function(){function o(e){t(this,o),this.ctx=e,this.ie=!1;var n=window.navigator.userAgent;(n.indexOf("MSIE")>-1||n.indexOf("Trident")>-1)&&(this.ie=!0)}return n(o,[{key:"log",value:function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"debug",r=this.opt.log;this.opt.debug&&"object"===(void 0===r?"undefined":e(r))&&"function"==typeof r[n]&&r[n]("mark.js: "+t)}},{key:"escapeStr",value:function(e){return e.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")}},{key:"createRegExp",value:function(e){return"disabled"!==this.opt.wildcards&&(e=this.setupWildcardsRegExp(e)),e=this.escapeStr(e),Object.keys(this.opt.synonyms).length&&(e=this.createSynonymsRegExp(e)),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),this.opt.diacritics&&(e=this.createDiacriticsRegExp(e)),e=this.createMergedBlanksRegExp(e),(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.createJoinersRegExp(e)),"disabled"!==this.opt.wildcards&&(e=this.createWildcardsRegExp(e)),e=this.createAccuracyRegExp(e)}},{key:"createSynonymsRegExp",value:function(e){var t=this.opt.synonyms,n=this.opt.caseSensitive?"":"i",r=this.opt.ignoreJoiners||this.opt.ignorePunctuation.length?"\0":"";for(var a in t)if(t.hasOwnProperty(a)){var o=t[a],i="disabled"!==this.opt.wildcards?this.setupWildcardsRegExp(a):this.escapeStr(a),s="disabled"!==this.opt.wildcards?this.setupWildcardsRegExp(o):this.escapeStr(o);""!==i&&""!==s&&(e=e.replace(new RegExp("("+this.escapeStr(i)+"|"+this.escapeStr(s)+")","gm"+n),r+"("+this.processSynomyms(i)+"|"+this.processSynomyms(s)+")"+r))}return e}},{key:"processSynomyms",value:function(e){return(this.opt.ignoreJoiners||this.opt.ignorePunctuation.length)&&(e=this.setupIgnoreJoinersRegExp(e)),e}},{key:"setupWildcardsRegExp",value:function(e){return(e=e.replace(/(?:\\)*\?/g,function(e){return"\\"===e.charAt(0)?"?":"\x01"})).replace(/(?:\\)*\*/g,function(e){return"\\"===e.charAt(0)?"*":"\x02"})}},{key:"createWildcardsRegExp",value:function(e){var t="withSpaces"===this.opt.wildcards;return e.replace(/\u0001/g,t?"[\\S\\s]?":"\\S?").replace(/\u0002/g,t?"[\\S\\s]*?":"\\S*")}},{key:"setupIgnoreJoinersRegExp",value:function(e){return e.replace(/[^(|)\\]/g,function(e,t,n){var r=n.charAt(t+1);return/[(|)\\]/.test(r)||""===r?e:e+"\0"})}},{key:"createJoinersRegExp",value:function(e){var t=[],n=this.opt.ignorePunctuation;return Array.isArray(n)&&n.length&&t.push(this.escapeStr(n.join(""))),this.opt.ignoreJoiners&&t.push("\\u00ad\\u200b\\u200c\\u200d"),t.length?e.split(/\u0000+/).join("["+t.join("")+"]*"):e}},{key:"createDiacriticsRegExp",value:function(e){var t=this.opt.caseSensitive?"":"i",n=this.opt.caseSensitive?["a\xe0\xe1\u1ea3\xe3\u1ea1\u0103\u1eb1\u1eaf\u1eb3\u1eb5\u1eb7\xe2\u1ea7\u1ea5\u1ea9\u1eab\u1ead\xe4\xe5\u0101\u0105","A\xc0\xc1\u1ea2\xc3\u1ea0\u0102\u1eb0\u1eae\u1eb2\u1eb4\u1eb6\xc2\u1ea6\u1ea4\u1ea8\u1eaa\u1eac\xc4\xc5\u0100\u0104","c\xe7\u0107\u010d","C\xc7\u0106\u010c","d\u0111\u010f","D\u0110\u010e","e\xe8\xe9\u1ebb\u1ebd\u1eb9\xea\u1ec1\u1ebf\u1ec3\u1ec5\u1ec7\xeb\u011b\u0113\u0119","E\xc8\xc9\u1eba\u1ebc\u1eb8\xca\u1ec0\u1ebe\u1ec2\u1ec4\u1ec6\xcb\u011a\u0112\u0118","i\xec\xed\u1ec9\u0129\u1ecb\xee\xef\u012b","I\xcc\xcd\u1ec8\u0128\u1eca\xce\xcf\u012a","l\u0142","L\u0141","n\xf1\u0148\u0144","N\xd1\u0147\u0143","o\xf2\xf3\u1ecf\xf5\u1ecd\xf4\u1ed3\u1ed1\u1ed5\u1ed7\u1ed9\u01a1\u1edf\u1ee1\u1edb\u1edd\u1ee3\xf6\xf8\u014d","O\xd2\xd3\u1ece\xd5\u1ecc\xd4\u1ed2\u1ed0\u1ed4\u1ed6\u1ed8\u01a0\u1ede\u1ee0\u1eda\u1edc\u1ee2\xd6\xd8\u014c","r\u0159","R\u0158","s\u0161\u015b\u0219\u015f","S\u0160\u015a\u0218\u015e","t\u0165\u021b\u0163","T\u0164\u021a\u0162","u\xf9\xfa\u1ee7\u0169\u1ee5\u01b0\u1eeb\u1ee9\u1eed\u1eef\u1ef1\xfb\xfc\u016f\u016b","U\xd9\xda\u1ee6\u0168\u1ee4\u01af\u1eea\u1ee8\u1eec\u1eee\u1ef0\xdb\xdc\u016e\u016a","y\xfd\u1ef3\u1ef7\u1ef9\u1ef5\xff","Y\xdd\u1ef2\u1ef6\u1ef8\u1ef4\u0178","z\u017e\u017c\u017a","Z\u017d\u017b\u0179"]:["a\xe0\xe1\u1ea3\xe3\u1ea1\u0103\u1eb1\u1eaf\u1eb3\u1eb5\u1eb7\xe2\u1ea7\u1ea5\u1ea9\u1eab\u1ead\xe4\xe5\u0101\u0105A\xc0\xc1\u1ea2\xc3\u1ea0\u0102\u1eb0\u1eae\u1eb2\u1eb4\u1eb6\xc2\u1ea6\u1ea4\u1ea8\u1eaa\u1eac\xc4\xc5\u0100\u0104","c\xe7\u0107\u010dC\xc7\u0106\u010c","d\u0111\u010fD\u0110\u010e","e\xe8\xe9\u1ebb\u1ebd\u1eb9\xea\u1ec1\u1ebf\u1ec3\u1ec5\u1ec7\xeb\u011b\u0113\u0119E\xc8\xc9\u1eba\u1ebc\u1eb8\xca\u1ec0\u1ebe\u1ec2\u1ec4\u1ec6\xcb\u011a\u0112\u0118","i\xec\xed\u1ec9\u0129\u1ecb\xee\xef\u012bI\xcc\xcd\u1ec8\u0128\u1eca\xce\xcf\u012a","l\u0142L\u0141","n\xf1\u0148\u0144N\xd1\u0147\u0143","o\xf2\xf3\u1ecf\xf5\u1ecd\xf4\u1ed3\u1ed1\u1ed5\u1ed7\u1ed9\u01a1\u1edf\u1ee1\u1edb\u1edd\u1ee3\xf6\xf8\u014dO\xd2\xd3\u1ece\xd5\u1ecc\xd4\u1ed2\u1ed0\u1ed4\u1ed6\u1ed8\u01a0\u1ede\u1ee0\u1eda\u1edc\u1ee2\xd6\xd8\u014c","r\u0159R\u0158","s\u0161\u015b\u0219\u015fS\u0160\u015a\u0218\u015e","t\u0165\u021b\u0163T\u0164\u021a\u0162","u\xf9\xfa\u1ee7\u0169\u1ee5\u01b0\u1eeb\u1ee9\u1eed\u1eef\u1ef1\xfb\xfc\u016f\u016bU\xd9\xda\u1ee6\u0168\u1ee4\u01af\u1eea\u1ee8\u1eec\u1eee\u1ef0\xdb\xdc\u016e\u016a","y\xfd\u1ef3\u1ef7\u1ef9\u1ef5\xffY\xdd\u1ef2\u1ef6\u1ef8\u1ef4\u0178","z\u017e\u017c\u017aZ\u017d\u017b\u0179"],r=[];return e.split("").forEach(function(a){n.every(function(n){if(-1!==n.indexOf(a)){if(r.indexOf(n)>-1)return!1;e=e.replace(new RegExp("["+n+"]","gm"+t),"["+n+"]"),r.push(n)}return!0})}),e}},{key:"createMergedBlanksRegExp",value:function(e){return e.replace(/[\s]+/gim,"[\\s]+")}},{key:"createAccuracyRegExp",value:function(e){var t=this,n="!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~\xa1\xbf",r=this.opt.accuracy,a="string"==typeof r?r:r.value,o="string"==typeof r?[]:r.limiters,i="";switch(o.forEach(function(e){i+="|"+t.escapeStr(e)}),a){case"partially":default:return"()("+e+")";case"complementary":return"()([^"+(i="\\s"+(i||this.escapeStr(n)))+"]*"+e+"[^"+i+"]*)";case"exactly":return"(^|\\s"+i+")("+e+")(?=$|\\s"+i+")"}}},{key:"getSeparatedKeywords",value:function(e){var t=this,n=[];return e.forEach(function(e){t.opt.separateWordSearch?e.split(" ").forEach(function(e){e.trim()&&-1===n.indexOf(e)&&n.push(e)}):e.trim()&&-1===n.indexOf(e)&&n.push(e)}),{keywords:n.sort(function(e,t){return t.length-e.length}),length:n.length}}},{key:"isNumeric",value:function(e){return Number(parseFloat(e))==e}},{key:"checkRanges",value:function(e){var t=this;if(!Array.isArray(e)||"[object Object]"!==Object.prototype.toString.call(e[0]))return this.log("markRanges() will only accept an array of objects"),this.opt.noMatch(e),[];var n=[],r=0;return e.sort(function(e,t){return e.start-t.start}).forEach(function(e){var a=t.callNoMatchOnInvalidRanges(e,r),o=a.start,i=a.end;a.valid&&(e.start=o,e.length=i-o,n.push(e),r=i)}),n}},{key:"callNoMatchOnInvalidRanges",value:function(e,t){var n=void 0,r=void 0,a=!1;return e&&void 0!==e.start?(r=(n=parseInt(e.start,10))+parseInt(e.length,10),this.isNumeric(e.start)&&this.isNumeric(e.length)&&r-t>0&&r-n>0?a=!0:(this.log("Ignoring invalid or overlapping range: "+JSON.stringify(e)),this.opt.noMatch(e))):(this.log("Ignoring invalid range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:n,end:r,valid:a}}},{key:"checkWhitespaceRanges",value:function(e,t,n){var r=void 0,a=!0,o=n.length,i=t-o,s=parseInt(e.start,10)-i;return(r=(s=s>o?o:s)+parseInt(e.length,10))>o&&(r=o,this.log("End range automatically set to the max value of "+o)),s<0||r-s<0||s>o||r>o?(a=!1,this.log("Invalid range: "+JSON.stringify(e)),this.opt.noMatch(e)):""===n.substring(s,r).replace(/\s+/g,"")&&(a=!1,this.log("Skipping whitespace only range: "+JSON.stringify(e)),this.opt.noMatch(e)),{start:s,end:r,valid:a}}},{key:"getTextNodes",value:function(e){var t=this,n="",r=[];this.iterator.forEachNode(NodeFilter.SHOW_TEXT,function(e){r.push({start:n.length,end:(n+=e.textContent).length,node:e})},function(e){return t.matchesExclude(e.parentNode)?NodeFilter.FILTER_REJECT:NodeFilter.FILTER_ACCEPT},function(){e({value:n,nodes:r})})}},{key:"matchesExclude",value:function(e){return a.matches(e,this.opt.exclude.concat(["script","style","title","head","html"]))}},{key:"wrapRangeInTextNode",value:function(e,t,n){var r=this.opt.element?this.opt.element:"mark",a=e.splitText(t),o=a.splitText(n-t),i=document.createElement(r);return i.setAttribute("data-markjs","true"),this.opt.className&&i.setAttribute("class",this.opt.className),i.textContent=a.textContent,a.parentNode.replaceChild(i,a),o}},{key:"wrapRangeInMappedTextNode",value:function(e,t,n,r,a){var o=this;e.nodes.every(function(i,s){var c=e.nodes[s+1];if(void 0===c||c.start>t){if(!r(i.node))return!1;var l=t-i.start,u=(n>i.end?i.end:n)-i.start,d=e.value.substr(0,i.start),p=e.value.substr(u+i.start);if(i.node=o.wrapRangeInTextNode(i.node,l,u),e.value=d+p,e.nodes.forEach(function(t,n){n>=s&&(e.nodes[n].start>0&&n!==s&&(e.nodes[n].start-=u),e.nodes[n].end-=u)}),n-=u,a(i.node.previousSibling,i.start),!(n>i.end))return!1;t=i.end}return!0})}},{key:"wrapMatches",value:function(e,t,n,r,a){var o=this,i=0===t?0:t+1;this.getTextNodes(function(t){t.nodes.forEach(function(t){t=t.node;for(var a=void 0;null!==(a=e.exec(t.textContent))&&""!==a[i];)if(n(a[i],t)){var s=a.index;if(0!==i)for(var c=1;c
'};function a(e,t,n){return en?n:e}function o(e){return 100*(-1+e)}function i(e,t,n){var a;return(a="translate3d"===r.positionUsing?{transform:"translate3d("+o(e)+"%,0,0)"}:"translate"===r.positionUsing?{transform:"translate("+o(e)+"%,0)"}:{"margin-left":o(e)+"%"}).transition="all "+t+"ms "+n,a}n.configure=function(e){var t,n;for(t in e)void 0!==(n=e[t])&&e.hasOwnProperty(t)&&(r[t]=n);return this},n.status=null,n.set=function(e){var t=n.isStarted();e=a(e,r.minimum,1),n.status=1===e?null:e;var o=n.render(!t),l=o.querySelector(r.barSelector),u=r.speed,d=r.easing;return o.offsetWidth,s(function(t){""===r.positionUsing&&(r.positionUsing=n.getPositioningCSS()),c(l,i(e,u,d)),1===e?(c(o,{transition:"none",opacity:1}),o.offsetWidth,setTimeout(function(){c(o,{transition:"all "+u+"ms linear",opacity:0}),setTimeout(function(){n.remove(),t()},u)},u)):setTimeout(t,u)}),this},n.isStarted=function(){return"number"==typeof n.status},n.start=function(){n.status||n.set(0);var e=function(){setTimeout(function(){n.status&&(n.trickle(),e())},r.trickleSpeed)};return r.trickle&&e(),this},n.done=function(e){return e||n.status?n.inc(.3+.5*Math.random()).set(1):this},n.inc=function(e){var t=n.status;return t?("number"!=typeof e&&(e=(1-t)*a(Math.random()*t,.1,.95)),t=a(t+e,0,.994),n.set(t)):n.start()},n.trickle=function(){return n.inc(Math.random()*r.trickleRate)},e=0,t=0,n.promise=function(r){return r&&"resolved"!==r.state()?(0===t&&n.start(),e++,t++,r.always(function(){0===--t?(e=0,n.done()):n.set((e-t)/e)}),this):this},n.render=function(e){if(n.isRendered())return document.getElementById("nprogress");u(document.documentElement,"nprogress-busy");var t=document.createElement("div");t.id="nprogress",t.innerHTML=r.template;var a,i=t.querySelector(r.barSelector),s=e?"-100":o(n.status||0),l=document.querySelector(r.parent);return c(i,{transition:"all 0 linear",transform:"translate3d("+s+"%,0,0)"}),r.showSpinner||(a=t.querySelector(r.spinnerSelector))&&f(a),l!=document.body&&u(l,"nprogress-custom-parent"),l.appendChild(t),t},n.remove=function(){d(document.documentElement,"nprogress-busy"),d(document.querySelector(r.parent),"nprogress-custom-parent");var e=document.getElementById("nprogress");e&&f(e)},n.isRendered=function(){return!!document.getElementById("nprogress")},n.getPositioningCSS=function(){var e=document.body.style,t="WebkitTransform"in e?"Webkit":"MozTransform"in e?"Moz":"msTransform"in e?"ms":"OTransform"in e?"O":"";return t+"Perspective"in e?"translate3d":t+"Transform"in e?"translate":"margin"};var s=function(){var e=[];function t(){var n=e.shift();n&&n(t)}return function(n){e.push(n),1==e.length&&t()}}(),c=function(){var e=["Webkit","O","Moz","ms"],t={};function n(e){return e.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(e,t){return t.toUpperCase()})}function r(t){var n=document.body.style;if(t in n)return t;for(var r,a=e.length,o=t.charAt(0).toUpperCase()+t.slice(1);a--;)if((r=e[a]+o)in n)return r;return t}function a(e){return e=n(e),t[e]||(t[e]=r(e))}function o(e,t,n){t=a(t),e.style[t]=n}return function(e,t){var n,r,a=arguments;if(2==a.length)for(n in t)void 0!==(r=t[n])&&t.hasOwnProperty(n)&&o(e,n,r);else o(e,a[1],a[2])}}();function l(e,t){return("string"==typeof e?e:p(e)).indexOf(" "+t+" ")>=0}function u(e,t){var n=p(e),r=n+t;l(n,t)||(e.className=r.substring(1))}function d(e,t){var n,r=p(e);l(e,t)&&(n=r.replace(" "+t+" "," "),e.className=n.substring(1,n.length-1))}function p(e){return(" "+(e.className||"")+" ").replace(/\s+/gi," ")}function f(e){e&&e.parentNode&&e.parentNode.removeChild(e)}return n},void 0===(a="function"==typeof r?r.call(t,n,t,e):r)||(e.exports=a)},54785(e,t,n){var r=n(5526);e.exports=h,e.exports.parse=o,e.exports.compile=function(e,t){return l(o(e,t),t)},e.exports.tokensToFunction=l,e.exports.tokensToRegExp=g;var a=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");function o(e,t){for(var n,r=[],o=0,s=0,c="",l=t&&t.delimiter||"/";null!=(n=a.exec(e));){var u=n[0],p=n[1],f=n.index;if(c+=e.slice(s,f),s=f+u.length,p)c+=p[1];else{var g=e[s],h=n[2],m=n[3],b=n[4],y=n[5],v=n[6],x=n[7];c&&(r.push(c),c="");var w=null!=h&&null!=g&&g!==h,S="+"===v||"*"===v,k="?"===v||"*"===v,_=h||l,E=b||y,C=h||("string"==typeof r[r.length-1]?r[r.length-1]:"");r.push({name:m||o++,prefix:h||"",delimiter:_,optional:k,repeat:S,partial:w,asterisk:!!x,pattern:E?d(E):x?".*":i(_,C)})}}return s-1?"[^"+u(e)+"]+?":u(t)+"|(?:(?!"+u(t)+")[^"+u(e)+"])+?"}function s(e){return encodeURI(e).replace(/[\/?#]/g,function(e){return"%"+e.charCodeAt(0).toString(16).toUpperCase()})}function c(e){return encodeURI(e).replace(/[?#]/g,function(e){return"%"+e.charCodeAt(0).toString(16).toUpperCase()})}function l(e,t){for(var n=new Array(e.length),a=0;a>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b[\w-]+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?:\.\w+)*(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},parameter:{pattern:/(^|\s)-{1,2}(?:\w+:[+-]?)?\w+(?:\.\w+)*(?=[=\s]|$)/,alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:r},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0,inside:{bash:n}},{pattern:/(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,lookbehind:!0,greedy:!0,inside:r},{pattern:/(^|[^$\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\$'(?:[^'\\]|\\[\s\S])*'/,greedy:!0,inside:{entity:r.entity}}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:r.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:false|true)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}},n.inside=e.languages.bash;for(var a=["comment","function-name","for-or-select","assign-left","parameter","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],o=r.variable[1].inside,i=0;i>/g,function(e,n){return"(?:"+t[+n]+")"})}function n(e,n,r){return RegExp(t(e,n),r||"")}function r(e,t){for(var n=0;n>/g,function(){return"(?:"+e+")"});return e.replace(/<>/g,"[^\\s\\S]")}var a="bool byte char decimal double dynamic float int long object sbyte short string uint ulong ushort var void",o="class enum interface record struct",i="add alias and ascending async await by descending from(?=\\s*(?:\\w|$)) get global group into init(?=\\s*;) join let nameof not notnull on or orderby partial remove select set unmanaged value when where with(?=\\s*{)",s="abstract as base break case catch checked const continue default delegate do else event explicit extern finally fixed for foreach goto if implicit in internal is lock namespace new null operator out override params private protected public readonly ref return sealed sizeof stackalloc static switch this throw try typeof unchecked unsafe using virtual volatile while yield";function c(e){return"\\b(?:"+e.trim().replace(/ /g,"|")+")\\b"}var l=c(o),u=RegExp(c(a+" "+o+" "+i+" "+s)),d=c(o+" "+i+" "+s),p=c(a+" "+o+" "+s),f=r(/<(?:[^<>;=+\-*/%&|^]|<>)*>/.source,2),g=r(/\((?:[^()]|<>)*\)/.source,2),h=/@?\b[A-Za-z_]\w*\b/.source,m=t(/<<0>>(?:\s*<<1>>)?/.source,[h,f]),b=t(/(?!<<0>>)<<1>>(?:\s*\.\s*<<1>>)*/.source,[d,m]),y=/\[\s*(?:,\s*)*\]/.source,v=t(/<<0>>(?:\s*(?:\?\s*)?<<1>>)*(?:\s*\?)?/.source,[b,y]),x=t(/[^,()<>[\];=+\-*/%&|^]|<<0>>|<<1>>|<<2>>/.source,[f,g,y]),w=t(/\(<<0>>+(?:,<<0>>+)+\)/.source,[x]),S=t(/(?:<<0>>|<<1>>)(?:\s*(?:\?\s*)?<<2>>)*(?:\s*\?)?/.source,[w,b,y]),k={keyword:u,punctuation:/[<>()?,.:[\]]/},_=/'(?:[^\r\n'\\]|\\.|\\[Uux][\da-fA-F]{1,8})'/.source,E=/"(?:\\.|[^\\"\r\n])*"/.source,C=/@"(?:""|\\[\s\S]|[^\\"])*"(?!")/.source;e.languages.csharp=e.languages.extend("clike",{string:[{pattern:n(/(^|[^$\\])<<0>>/.source,[C]),lookbehind:!0,greedy:!0},{pattern:n(/(^|[^@$\\])<<0>>/.source,[E]),lookbehind:!0,greedy:!0}],"class-name":[{pattern:n(/(\busing\s+static\s+)<<0>>(?=\s*;)/.source,[b]),lookbehind:!0,inside:k},{pattern:n(/(\busing\s+<<0>>\s*=\s*)<<1>>(?=\s*;)/.source,[h,S]),lookbehind:!0,inside:k},{pattern:n(/(\busing\s+)<<0>>(?=\s*=)/.source,[h]),lookbehind:!0},{pattern:n(/(\b<<0>>\s+)<<1>>/.source,[l,m]),lookbehind:!0,inside:k},{pattern:n(/(\bcatch\s*\(\s*)<<0>>/.source,[b]),lookbehind:!0,inside:k},{pattern:n(/(\bwhere\s+)<<0>>/.source,[h]),lookbehind:!0},{pattern:n(/(\b(?:is(?:\s+not)?|as)\s+)<<0>>/.source,[v]),lookbehind:!0,inside:k},{pattern:n(/\b<<0>>(?=\s+(?!<<1>>|with\s*\{)<<2>>(?:\s*[=,;:{)\]]|\s+(?:in|when)\b))/.source,[S,p,h]),inside:k}],keyword:u,number:/(?:\b0(?:x[\da-f_]*[\da-f]|b[01_]*[01])|(?:\B\.\d+(?:_+\d+)*|\b\d+(?:_+\d+)*(?:\.\d+(?:_+\d+)*)?)(?:e[-+]?\d+(?:_+\d+)*)?)(?:[dflmu]|lu|ul)?\b/i,operator:/>>=?|<<=?|[-=]>|([-+&|])\1|~|\?\?=?|[-+*/%&|^!=<>]=?/,punctuation:/\?\.?|::|[{}[\];(),.:]/}),e.languages.insertBefore("csharp","number",{range:{pattern:/\.\./,alias:"operator"}}),e.languages.insertBefore("csharp","punctuation",{"named-parameter":{pattern:n(/([(,]\s*)<<0>>(?=\s*:)/.source,[h]),lookbehind:!0,alias:"punctuation"}}),e.languages.insertBefore("csharp","class-name",{namespace:{pattern:n(/(\b(?:namespace|using)\s+)<<0>>(?:\s*\.\s*<<0>>)*(?=\s*[;{])/.source,[h]),lookbehind:!0,inside:{punctuation:/\./}},"type-expression":{pattern:n(/(\b(?:default|sizeof|typeof)\s*\(\s*(?!\s))(?:[^()\s]|\s(?!\s)|<<0>>)*(?=\s*\))/.source,[g]),lookbehind:!0,alias:"class-name",inside:k},"return-type":{pattern:n(/<<0>>(?=\s+(?:<<1>>\s*(?:=>|[({]|\.\s*this\s*\[)|this\s*\[))/.source,[S,b]),inside:k,alias:"class-name"},"constructor-invocation":{pattern:n(/(\bnew\s+)<<0>>(?=\s*[[({])/.source,[S]),lookbehind:!0,inside:k,alias:"class-name"},"generic-method":{pattern:n(/<<0>>\s*<<1>>(?=\s*\()/.source,[h,f]),inside:{function:n(/^<<0>>/.source,[h]),generic:{pattern:RegExp(f),alias:"class-name",inside:k}}},"type-list":{pattern:n(/\b((?:<<0>>\s+<<1>>|record\s+<<1>>\s*<<5>>|where\s+<<2>>)\s*:\s*)(?:<<3>>|<<4>>|<<1>>\s*<<5>>|<<6>>)(?:\s*,\s*(?:<<3>>|<<4>>|<<6>>))*(?=\s*(?:where|[{;]|=>|$))/.source,[l,m,h,S,u.source,g,/\bnew\s*\(\s*\)/.source]),lookbehind:!0,inside:{"record-arguments":{pattern:n(/(^(?!new\s*\()<<0>>\s*)<<1>>/.source,[m,g]),lookbehind:!0,greedy:!0,inside:e.languages.csharp},keyword:u,"class-name":{pattern:RegExp(S),greedy:!0,inside:k},punctuation:/[,()]/}},preprocessor:{pattern:/(^[\t ]*)#.*/m,lookbehind:!0,alias:"property",inside:{directive:{pattern:/(#)\b(?:define|elif|else|endif|endregion|error|if|line|nullable|pragma|region|undef|warning)\b/,lookbehind:!0,alias:"keyword"}}}});var T=E+"|"+_,A=t(/\/(?![*/])|\/\/[^\r\n]*[\r\n]|\/\*(?:[^*]|\*(?!\/))*\*\/|<<0>>/.source,[T]),P=r(t(/[^"'/()]|<<0>>|\(<>*\)/.source,[A]),2),N=/\b(?:assembly|event|field|method|module|param|property|return|type)\b/.source,j=t(/<<0>>(?:\s*\(<<1>>*\))?/.source,[b,P]);e.languages.insertBefore("csharp","class-name",{attribute:{pattern:n(/((?:^|[^\s\w>)?])\s*\[\s*)(?:<<0>>\s*:\s*)?<<1>>(?:\s*,\s*<<1>>)*(?=\s*\])/.source,[N,j]),lookbehind:!0,greedy:!0,inside:{target:{pattern:n(/^<<0>>(?=\s*:)/.source,[N]),alias:"keyword"},"attribute-arguments":{pattern:n(/\(<<0>>*\)/.source,[P]),inside:e.languages.csharp},"class-name":{pattern:RegExp(b),inside:{punctuation:/\./}},punctuation:/[:,]/}}});var L=/:[^}\r\n]+/.source,O=r(t(/[^"'/()]|<<0>>|\(<>*\)/.source,[A]),2),R=t(/\{(?!\{)(?:(?![}:])<<0>>)*<<1>>?\}/.source,[O,L]),I=r(t(/[^"'/()]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|<<0>>|\(<>*\)/.source,[T]),2),D=t(/\{(?!\{)(?:(?![}:])<<0>>)*<<1>>?\}/.source,[I,L]);function F(t,r){return{interpolation:{pattern:n(/((?:^|[^{])(?:\{\{)*)<<0>>/.source,[t]),lookbehind:!0,inside:{"format-string":{pattern:n(/(^\{(?:(?![}:])<<0>>)*)<<1>>(?=\}$)/.source,[r,L]),lookbehind:!0,inside:{punctuation:/^:/}},punctuation:/^\{|\}$/,expression:{pattern:/[\s\S]+/,alias:"language-csharp",inside:e.languages.csharp}}},string:/[\s\S]+/}}e.languages.insertBefore("csharp","string",{"interpolation-string":[{pattern:n(/(^|[^\\])(?:\$@|@\$)"(?:""|\\[\s\S]|\{\{|<<0>>|[^\\{"])*"/.source,[R]),lookbehind:!0,greedy:!0,inside:F(R,O)},{pattern:n(/(^|[^@\\])\$"(?:\\.|\{\{|<<0>>|[^\\"{])*"/.source,[D]),lookbehind:!0,greedy:!0,inside:F(D,I)}],char:{pattern:RegExp(_),greedy:!0}}),e.languages.dotnet=e.languages.cs=e.languages.csharp}(Prism)},67874(){Prism.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},Prism.languages.webmanifest=Prism.languages.json},88020(){!function(e){function t(e,t){return"___"+e.toUpperCase()+t+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(n,r,a,o){if(n.language===r){var i=n.tokenStack=[];n.code=n.code.replace(a,function(e){if("function"==typeof o&&!o(e))return e;for(var a,s=i.length;-1!==n.code.indexOf(a=t(r,s));)++s;return i[s]=e,a}),n.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(n,r){if(n.language===r&&n.tokenStack){n.grammar=e.languages[r];var a=0,o=Object.keys(n.tokenStack);!function i(s){for(var c=0;c=o.length);c++){var l=s[c];if("string"==typeof l||l.content&&"string"==typeof l.content){var u=o[a],d=n.tokenStack[u],p="string"==typeof l?l:l.content,f=t(r,u),g=p.indexOf(f);if(g>-1){++a;var h=p.substring(0,g),m=new e.Token(r,e.tokenize(d,n.grammar),"language-"+r,d),b=p.substring(g+f.length),y=[];h&&y.push.apply(y,i([h])),y.push(m),b&&y.push.apply(y,i([b])),"string"==typeof l?s.splice.apply(s,[c,1].concat(y)):l.content=y}}else l.content&&i(l.content)}return s}(n.tokens)}}}})}(Prism)},69656(){Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))}),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(e,t){var n={};n["language-"+t]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[t]},n.cdata=/^$/i;var r={"included-cdata":{pattern://i,inside:n}};r["language-"+t]={pattern:/[\s\S]+/,inside:Prism.languages[t]};var a={};a[e]={pattern:RegExp(/(<__[^>]*>)(?:))*\]\]>|(?!)/.source.replace(/__/g,function(){return e}),"i"),lookbehind:!0,greedy:!0,inside:r},Prism.languages.insertBefore("markup","cdata",a)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(e,t){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+e+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[t,"language-"+t],inside:Prism.languages[t]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml},16186(e,t,n){"use strict";var r=n(62985);function a(){}function o(){}o.resetWarningCache=a,e.exports=function(){function e(e,t,n,a,o,i){if(i!==r){var s=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw s.name="Invariant Violation",s}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:o,resetWarningCache:a};return n.PropTypes=n,n}},2736(e,t,n){e.exports=n(16186)()},62985(e){"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},19481(e,t,n){"use strict";var r=n(30758),a=n(31896);function o(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n