diff --git a/.vscode/launch.json b/.vscode/launch.json index f1f259a..d5db6fc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -65,6 +65,26 @@ "stopAtEntry": false, "type": "coreclr" }, + { + "console": "integratedTerminal", + "cwd": "${workspaceFolder}/src/DotGraphExample", + "name": "DotGraphExample", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/DotGraphExample/bin/Debug/net10.0/DotGraphExample.dll", + "request": "launch", + "stopAtEntry": false, + "type": "coreclr" + }, + { + "console": "integratedTerminal", + "cwd": "${workspaceFolder}/src/DynamicProxyExample", + "name": "DynamicProxyExample", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/DynamicProxyExample/bin/Debug/net10.0/DynamicProxyExample.dll", + "request": "launch", + "stopAtEntry": false, + "type": "coreclr" + }, { "console": "integratedTerminal", "cwd": "${workspaceFolder}/src/GenericHostBuilderExample", @@ -75,6 +95,21 @@ "stopAtEntry": false, "type": "coreclr" }, + { + "cwd": "${workspaceFolder}/src/MultitenantExample.AspNetCore", + "launchSettingsProfile": "MultitenantExample.AspNetCore", + "name": "MultitenantExample.AspNetCore", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/MultitenantExample.AspNetCore/bin/Debug/net10.0/MultitenantExample.AspNetCore.dll", + "request": "launch", + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\\\bNow listening on:\\\\s+(https?://\\\\S+)", + "uriFormat": "%s/api/tenant?tenant=alpha" + }, + "stopAtEntry": false, + "type": "coreclr" + }, { "console": "integratedTerminal", "cwd": "${workspaceFolder}/src/MultitenantExample.ConsoleApplication", @@ -84,6 +119,16 @@ "request": "launch", "stopAtEntry": false, "type": "coreclr" + }, + { + "console": "integratedTerminal", + "cwd": "${workspaceFolder}/src/PoolingExample", + "name": "PoolingExample", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/PoolingExample/bin/Debug/net10.0/PoolingExample.dll", + "request": "launch", + "stopAtEntry": false, + "type": "coreclr" } ], "version": "0.2.0" diff --git a/Examples.slnx b/Examples.slnx index f2fb6b6..d07123f 100644 --- a/Examples.slnx +++ b/Examples.slnx @@ -3,14 +3,18 @@ + + + + diff --git a/README.md b/README.md index 4cbad45..9afac27 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Each example has its own README with what it shows and how to run it. | [AspNetCoreExample](src/AspNetCoreExample/README.md) | A `Startup` class whose `ConfigureContainer` takes a `ContainerBuilder` | [`Autofac.Extensions.DependencyInjection`](https://github.com/autofac/Autofac.Extensions.DependencyInjection) | | [AspNetCoreNoStartupExample](src/AspNetCoreNoStartupExample/README.md) | The same wiring in the minimal hosting model, with no `Startup` class | [`Autofac.Extensions.DependencyInjection`](https://github.com/autofac/Autofac.Extensions.DependencyInjection) | | [AspNetCoreChildLifetimeScope](src/AspNetCoreChildLifetimeScope/README.md) | Two hosts sharing one container, each rooted in its own child scope | [`Autofac.Extensions.DependencyInjection`](https://github.com/autofac/Autofac.Extensions.DependencyInjection) | +| [MultitenantExample.AspNetCore](src/MultitenantExample.AspNetCore/README.md) | Per-tenant registration overrides, with the tenant read from the query string | [`Autofac.AspNetCore.Multitenant`](https://github.com/autofac/Autofac.AspNetCore.Multitenant) | ### Hosting and core features @@ -24,6 +25,9 @@ Each example has its own README with what it shows and how to run it. | [ConfigurationExample](src/ConfigurationExample/README.md) | Registering from `autofac.json`, including an unreferenced plugin assembly | [`Autofac.Configuration`](https://github.com/autofac/Autofac.Configuration) | | [AttributeMetadataExample](src/AttributeMetadataExample/README.md) | Metadata by string, class, interface, and attribute, then filtering on it | [`Autofac.Extras.AttributeMetadata`](https://github.com/autofac/Autofac.Extras.AttributeMetadata) | | [MultitenantExample.ConsoleApplication](src/MultitenantExample.ConsoleApplication/README.md) | Per-tenant overrides with no web request in sight | [`Autofac.Multitenant`](https://github.com/autofac/Autofac.Multitenant) | +| [DynamicProxyExample](src/DynamicProxyExample/README.md) | Method interception, wired both on the registration and by attribute | [`Autofac.Extras.DynamicProxy`](https://github.com/autofac/Autofac.Extras.DynamicProxy) | +| [PoolingExample](src/PoolingExample/README.md) | Reusing expensive instances across scopes, with a reset-on-return policy | [`Autofac.Pooling`](https://github.com/autofac/Autofac.Pooling) | +| [DotGraphExample](src/DotGraphExample/README.md) | Capturing a resolve operation as a Graphviz graph for troubleshooting | [`Autofac.Diagnostics.DotGraph`](https://github.com/autofac/Autofac.Diagnostics.DotGraph) | ### .NET Framework diff --git a/src/DotGraphExample/BracketFormatter.cs b/src/DotGraphExample/BracketFormatter.cs new file mode 100644 index 0000000..7439cc6 --- /dev/null +++ b/src/DotGraphExample/BracketFormatter.cs @@ -0,0 +1,14 @@ +namespace DotGraphExample; + +/// +/// A decorator, so the captured graph shows an adapter chain rather than a flat +/// list of unrelated services. +/// +public sealed class BracketFormatter : IFormatter +{ + private readonly IFormatter _inner; + + public BracketFormatter(IFormatter inner) => _inner = inner; + + public string Format(string value) => $"[{_inner.Format(value)}]"; +} diff --git a/src/DotGraphExample/DataSource.cs b/src/DotGraphExample/DataSource.cs new file mode 100644 index 0000000..e3ff4a3 --- /dev/null +++ b/src/DotGraphExample/DataSource.cs @@ -0,0 +1,6 @@ +namespace DotGraphExample; + +public sealed class DataSource : IDataSource +{ + public string Read() => "42"; +} diff --git a/src/DotGraphExample/DotGraphExample.csproj b/src/DotGraphExample/DotGraphExample.csproj new file mode 100644 index 0000000..0e436cf --- /dev/null +++ b/src/DotGraphExample/DotGraphExample.csproj @@ -0,0 +1,14 @@ + + + + Exe + net10.0 + enable + + + + + + + + diff --git a/src/DotGraphExample/IDataSource.cs b/src/DotGraphExample/IDataSource.cs new file mode 100644 index 0000000..4d05258 --- /dev/null +++ b/src/DotGraphExample/IDataSource.cs @@ -0,0 +1,6 @@ +namespace DotGraphExample; + +public interface IDataSource +{ + string Read(); +} diff --git a/src/DotGraphExample/IFormatter.cs b/src/DotGraphExample/IFormatter.cs new file mode 100644 index 0000000..8c5927f --- /dev/null +++ b/src/DotGraphExample/IFormatter.cs @@ -0,0 +1,6 @@ +namespace DotGraphExample; + +public interface IFormatter +{ + string Format(string value); +} diff --git a/src/DotGraphExample/IReportGenerator.cs b/src/DotGraphExample/IReportGenerator.cs new file mode 100644 index 0000000..f00b8e2 --- /dev/null +++ b/src/DotGraphExample/IReportGenerator.cs @@ -0,0 +1,6 @@ +namespace DotGraphExample; + +public interface IReportGenerator +{ + string Generate(); +} diff --git a/src/DotGraphExample/Program.cs b/src/DotGraphExample/Program.cs new file mode 100644 index 0000000..5bd7fe0 --- /dev/null +++ b/src/DotGraphExample/Program.cs @@ -0,0 +1,42 @@ +using Autofac; +using Autofac.Diagnostics.DotGraph; + +namespace DotGraphExample; + +internal static class Program +{ + public static void Main() + { + var builder = new ContainerBuilder(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterDecorator(); + builder.RegisterType().As(); + + using var container = builder.Build(); + + // The tracer raises an event per completed resolve operation, and the + // trace content is a Graphviz DOT document describing that operation. + var tracer = new DotDiagnosticTracer(); + var graphs = new List(); + tracer.OperationCompleted += (sender, args) => graphs.Add(args.TraceContent); + container.SubscribeToDiagnostics(tracer); + + using (var scope = container.BeginLifetimeScope()) + { + Console.WriteLine($"Report: {scope.Resolve().Generate()}"); + } + + var output = Path.Combine(AppContext.BaseDirectory, "resolve-graph.dot"); + File.WriteAllText(output, string.Join(Environment.NewLine, graphs)); + + Console.WriteLine(); + Console.WriteLine($"Captured {graphs.Count} resolve operation(s), written to:"); + Console.WriteLine($" {output}"); + Console.WriteLine(); + Console.WriteLine("Render it with Graphviz:"); + Console.WriteLine($" dot -T png -O \"{output}\""); + Console.WriteLine(); + Console.WriteLine("Tracing is expensive. Turn it on while troubleshooting, not in production."); + } +} diff --git a/src/DotGraphExample/README.md b/src/DotGraphExample/README.md new file mode 100644 index 0000000..27b0a52 --- /dev/null +++ b/src/DotGraphExample/README.md @@ -0,0 +1,9 @@ +# DotGraphExample + +Capturing a resolve operation as a Graphviz graph, which is the fastest way to see what Autofac actually did when a resolve surprises you. The registrations include a decorator so the graph has an adapter chain worth looking at. + +Packages: [`Autofac`](https://github.com/autofac/Autofac), [`Autofac.Diagnostics.DotGraph`](https://github.com/autofac/Autofac.Diagnostics.DotGraph) + +Run `dotnet run --project src/DotGraphExample`. It writes a `.dot` file next to the built assembly and prints the Graphviz command to render it. + +See [Tracing](https://autofac.readthedocs.io/en/latest/troubleshooting/tracing.html) for the documentation this example follows. diff --git a/src/DotGraphExample/ReportGenerator.cs b/src/DotGraphExample/ReportGenerator.cs new file mode 100644 index 0000000..7265eb9 --- /dev/null +++ b/src/DotGraphExample/ReportGenerator.cs @@ -0,0 +1,19 @@ +namespace DotGraphExample; + +/// +/// Sits at the top of the graph, depending on a source and a formatter so the +/// trace has more than one level to draw. +/// +public sealed class ReportGenerator : IReportGenerator +{ + private readonly IDataSource _source; + private readonly IFormatter _formatter; + + public ReportGenerator(IDataSource source, IFormatter formatter) + { + _source = source; + _formatter = formatter; + } + + public string Generate() => _formatter.Format(_source.Read()); +} diff --git a/src/DotGraphExample/UppercaseFormatter.cs b/src/DotGraphExample/UppercaseFormatter.cs new file mode 100644 index 0000000..2986a27 --- /dev/null +++ b/src/DotGraphExample/UppercaseFormatter.cs @@ -0,0 +1,6 @@ +namespace DotGraphExample; + +public sealed class UppercaseFormatter : IFormatter +{ + public string Format(string value) => value.ToUpperInvariant(); +} diff --git a/src/DynamicProxyExample/Calculator.cs b/src/DynamicProxyExample/Calculator.cs new file mode 100644 index 0000000..d4e304f --- /dev/null +++ b/src/DynamicProxyExample/Calculator.cs @@ -0,0 +1,6 @@ +namespace DynamicProxyExample; + +public sealed class Calculator : ICalculator +{ + public int Add(int left, int right) => left + right; +} diff --git a/src/DynamicProxyExample/CallLogger.cs b/src/DynamicProxyExample/CallLogger.cs new file mode 100644 index 0000000..8419bd6 --- /dev/null +++ b/src/DynamicProxyExample/CallLogger.cs @@ -0,0 +1,22 @@ +using Castle.DynamicProxy; + +namespace DynamicProxyExample; + +/// +/// An interceptor sees every call made through the proxied interface. +/// runs the real implementation; anything +/// before or after that call is yours to do. +/// +public sealed class CallLogger : IInterceptor +{ + private readonly TextWriter _output; + + public CallLogger(TextWriter output) => _output = output; + + public void Intercept(IInvocation invocation) + { + _output.WriteLine($" -> {invocation.Method.Name}({string.Join(", ", invocation.Arguments)})"); + invocation.Proceed(); + _output.WriteLine($" <- {invocation.Method.Name} returned {invocation.ReturnValue}"); + } +} diff --git a/src/DynamicProxyExample/DynamicProxyExample.csproj b/src/DynamicProxyExample/DynamicProxyExample.csproj new file mode 100644 index 0000000..9520fee --- /dev/null +++ b/src/DynamicProxyExample/DynamicProxyExample.csproj @@ -0,0 +1,14 @@ + + + + Exe + net10.0 + enable + + + + + + + + diff --git a/src/DynamicProxyExample/Greeter.cs b/src/DynamicProxyExample/Greeter.cs new file mode 100644 index 0000000..e97dc95 --- /dev/null +++ b/src/DynamicProxyExample/Greeter.cs @@ -0,0 +1,6 @@ +namespace DynamicProxyExample; + +public sealed class Greeter : IGreeter +{ + public string Greet(string name) => $"Hello, {name}!"; +} diff --git a/src/DynamicProxyExample/ICalculator.cs b/src/DynamicProxyExample/ICalculator.cs new file mode 100644 index 0000000..e01de70 --- /dev/null +++ b/src/DynamicProxyExample/ICalculator.cs @@ -0,0 +1,10 @@ +namespace DynamicProxyExample; + +/// +/// Interception is wired up on the registration for this service, so the +/// interface itself needs to know nothing about it. +/// +public interface ICalculator +{ + int Add(int left, int right); +} diff --git a/src/DynamicProxyExample/IGreeter.cs b/src/DynamicProxyExample/IGreeter.cs new file mode 100644 index 0000000..d88c5d4 --- /dev/null +++ b/src/DynamicProxyExample/IGreeter.cs @@ -0,0 +1,13 @@ +using Autofac.Extras.DynamicProxy; + +namespace DynamicProxyExample; + +/// +/// The attribute names the interceptor here instead, which keeps the wiring next +/// to the contract rather than in the container configuration. +/// +[Intercept(typeof(CallLogger))] +public interface IGreeter +{ + string Greet(string name); +} diff --git a/src/DynamicProxyExample/Program.cs b/src/DynamicProxyExample/Program.cs new file mode 100644 index 0000000..d158598 --- /dev/null +++ b/src/DynamicProxyExample/Program.cs @@ -0,0 +1,39 @@ +using Autofac; +using Autofac.Extras.DynamicProxy; + +namespace DynamicProxyExample; + +internal static class Program +{ + public static void Main() + { + var builder = new ContainerBuilder(); + + // The interceptor is an ordinary registration, so it can take + // dependencies like anything else in the container. + builder.Register(_ => new CallLogger(Console.Out)); + + // Style one: the registration names the interceptor. + builder.RegisterType() + .As() + .EnableInterfaceInterceptors() + .InterceptedBy(typeof(CallLogger)); + + // Style two: IGreeter carries [Intercept], so the registration only has + // to opt in to interception at all. + builder.RegisterType() + .As() + .EnableInterfaceInterceptors(); + + using var container = builder.Build(); + + Console.WriteLine("Calling ICalculator.Add, intercepted by registration:"); + var sum = container.Resolve().Add(2, 3); + Console.WriteLine($"Result: {sum}"); + + Console.WriteLine(); + Console.WriteLine("Calling IGreeter.Greet, intercepted by attribute:"); + var greeting = container.Resolve().Greet("Autofac"); + Console.WriteLine($"Result: {greeting}"); + } +} diff --git a/src/DynamicProxyExample/README.md b/src/DynamicProxyExample/README.md new file mode 100644 index 0000000..80c8139 --- /dev/null +++ b/src/DynamicProxyExample/README.md @@ -0,0 +1,9 @@ +# DynamicProxyExample + +Method interception with Castle DynamicProxy, wired up two ways: named on the registration, and declared with an `[Intercept]` attribute on the contract. The interceptor is a normal registration, so it can take dependencies of its own. + +Packages: [`Autofac`](https://github.com/autofac/Autofac), [`Autofac.Extras.DynamicProxy`](https://github.com/autofac/Autofac.Extras.DynamicProxy) + +Run `dotnet run --project src/DynamicProxyExample`. It prints the intercepted calls around each method. + +See [Type Interceptors](https://autofac.readthedocs.io/en/latest/advanced/interceptors.html) for the documentation this example follows. diff --git a/src/MultitenantExample.AspNetCore/Controllers/TenantController.cs b/src/MultitenantExample.AspNetCore/Controllers/TenantController.cs new file mode 100644 index 0000000..ef1d26d --- /dev/null +++ b/src/MultitenantExample.AspNetCore/Controllers/TenantController.cs @@ -0,0 +1,30 @@ +using Autofac.Multitenant; +using Microsoft.AspNetCore.Mvc; + +namespace MultitenantExample.AspNetCore.Controllers; + +[ApiController] +[Route("api/[controller]")] +public sealed class TenantController : ControllerBase +{ + private readonly ITenantDependency _dependency; + private readonly ITenantIdentificationStrategy _strategy; + + public TenantController(ITenantDependency dependency, ITenantIdentificationStrategy strategy) + { + _dependency = dependency; + _strategy = strategy; + } + + [HttpGet] + public IActionResult Get() + { + _strategy.TryIdentifyTenant(out var tenantId); + + return Ok(new + { + Tenant = tenantId as string ?? "(default)", + Resolved = _dependency.Describe(), + }); + } +} diff --git a/src/MultitenantExample.AspNetCore/DefaultDependency.cs b/src/MultitenantExample.AspNetCore/DefaultDependency.cs new file mode 100644 index 0000000..94a1780 --- /dev/null +++ b/src/MultitenantExample.AspNetCore/DefaultDependency.cs @@ -0,0 +1,9 @@ +namespace MultitenantExample.AspNetCore; + +/// +/// What every tenant gets unless it registers an override. +/// +public sealed class DefaultDependency : ITenantDependency +{ + public string Describe() => "DefaultDependency, shared by every tenant without an override"; +} diff --git a/src/MultitenantExample.AspNetCore/ITenantDependency.cs b/src/MultitenantExample.AspNetCore/ITenantDependency.cs new file mode 100644 index 0000000..e3c846a --- /dev/null +++ b/src/MultitenantExample.AspNetCore/ITenantDependency.cs @@ -0,0 +1,6 @@ +namespace MultitenantExample.AspNetCore; + +public interface ITenantDependency +{ + string Describe(); +} diff --git a/src/MultitenantExample.AspNetCore/MultitenantContainerSetup.cs b/src/MultitenantExample.AspNetCore/MultitenantContainerSetup.cs new file mode 100644 index 0000000..0df6bff --- /dev/null +++ b/src/MultitenantExample.AspNetCore/MultitenantContainerSetup.cs @@ -0,0 +1,31 @@ +using Autofac; +using Autofac.Multitenant; + +namespace MultitenantExample.AspNetCore; + +/// +/// Tenant overrides are configured here rather than in Startup, because +/// they need the built application container to construct the tenant +/// identification strategy from. +/// +public static class MultitenantContainerSetup +{ + public static MultitenantContainer ConfigureMultitenantContainer(IContainer container) + { + var strategy = new QueryStringTenantIdentificationStrategy(container.Resolve()); + var multitenantContainer = new MultitenantContainer(strategy, container); + + foreach (var tenantId in new[] { "alpha", "beta" }) + { + var id = tenantId; + multitenantContainer.ConfigureTenant( + id, + builder => builder + .Register(_ => new TenantOverrideDependency(id)) + .As() + .InstancePerLifetimeScope()); + } + + return multitenantContainer; + } +} diff --git a/src/MultitenantExample.AspNetCore/MultitenantExample.AspNetCore.csproj b/src/MultitenantExample.AspNetCore/MultitenantExample.AspNetCore.csproj new file mode 100644 index 0000000..6230502 --- /dev/null +++ b/src/MultitenantExample.AspNetCore/MultitenantExample.AspNetCore.csproj @@ -0,0 +1,13 @@ + + + + ../../build/AspNet.ruleset + net10.0 + enable + + + + + + + diff --git a/src/MultitenantExample.AspNetCore/Program.cs b/src/MultitenantExample.AspNetCore/Program.cs new file mode 100644 index 0000000..b844b2a --- /dev/null +++ b/src/MultitenantExample.AspNetCore/Program.cs @@ -0,0 +1,17 @@ +namespace MultitenantExample.AspNetCore; + +public static class Program +{ + public static void Main(string[] args) + { + // AutofacMultitenantServiceProviderFactory takes the method that turns the + // built application container into a MultitenantContainer, which is where + // per-tenant overrides live. + Host.CreateDefaultBuilder(args) + .UseServiceProviderFactory( + new AutofacMultitenantServiceProviderFactory(MultitenantContainerSetup.ConfigureMultitenantContainer)) + .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()) + .Build() + .Run(); + } +} diff --git a/src/MultitenantExample.AspNetCore/Properties/launchSettings.json b/src/MultitenantExample.AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..ca516a9 --- /dev/null +++ b/src/MultitenantExample.AspNetCore/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "MultitenantExample.AspNetCore": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + }, + "launchBrowser": false + } + } +} diff --git a/src/MultitenantExample.AspNetCore/QueryStringTenantIdentificationStrategy.cs b/src/MultitenantExample.AspNetCore/QueryStringTenantIdentificationStrategy.cs new file mode 100644 index 0000000..9860b42 --- /dev/null +++ b/src/MultitenantExample.AspNetCore/QueryStringTenantIdentificationStrategy.cs @@ -0,0 +1,24 @@ +using Autofac.Multitenant; + +namespace MultitenantExample.AspNetCore; + +/// +/// Identifies the tenant from a ?tenant= query string value. A real +/// application would more likely use the host name, a claim, or a header, but the +/// shape of the strategy is the same: look at ambient request state and hand back +/// an identifier. +/// +public sealed class QueryStringTenantIdentificationStrategy : ITenantIdentificationStrategy +{ + private readonly IHttpContextAccessor _accessor; + + public QueryStringTenantIdentificationStrategy(IHttpContextAccessor accessor) => _accessor = accessor; + + public bool TryIdentifyTenant(out object? tenantId) + { + tenantId = _accessor.HttpContext?.Request.Query["tenant"].FirstOrDefault(); + + // Returning false means "no tenant", and the default tenant is used. + return tenantId is string id && !string.IsNullOrWhiteSpace(id); + } +} diff --git a/src/MultitenantExample.AspNetCore/README.md b/src/MultitenantExample.AspNetCore/README.md new file mode 100644 index 0000000..26c7bcd --- /dev/null +++ b/src/MultitenantExample.AspNetCore/README.md @@ -0,0 +1,9 @@ +# MultitenantExample.AspNetCore + +Per-tenant registration overrides in an ASP.NET Core application, with the tenant taken from a `?tenant=` query string. Tenants without an override fall back to the container-wide default, which is the behaviour most people want to confirm. + +Packages: [`Autofac.AspNetCore.Multitenant`](https://github.com/autofac/Autofac.AspNetCore.Multitenant) + +Run `dotnet run --project src/MultitenantExample.AspNetCore`, then compare , , and . + +See [Multitenant Applications](https://autofac.readthedocs.io/en/latest/advanced/multitenant.html) for the documentation this example follows. diff --git a/src/MultitenantExample.AspNetCore/Startup.cs b/src/MultitenantExample.AspNetCore/Startup.cs new file mode 100644 index 0000000..bb8ca71 --- /dev/null +++ b/src/MultitenantExample.AspNetCore/Startup.cs @@ -0,0 +1,37 @@ +using Autofac; +using Autofac.Multitenant; + +namespace MultitenantExample.AspNetCore; + +public sealed class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + // AddAutofacMultitenantRequestServices swaps the request's service + // provider for the tenant's one. Without it every request resolves from + // the application container and tenant overrides never apply. + services + .AddAutofacMultitenantRequestServices() + .AddHttpContextAccessor() + .AddControllers(); + } + + public void ConfigureContainer(ContainerBuilder builder) + { + // Registrations shared by all tenants, including the default that + // tenant-specific registrations override. + builder.RegisterType() + .As() + .InstancePerLifetimeScope(); + + builder.RegisterType() + .As() + .SingleInstance(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + app.UseEndpoints(endpoints => endpoints.MapControllers()); + } +} diff --git a/src/MultitenantExample.AspNetCore/TenantOverrideDependency.cs b/src/MultitenantExample.AspNetCore/TenantOverrideDependency.cs new file mode 100644 index 0000000..4c5cc83 --- /dev/null +++ b/src/MultitenantExample.AspNetCore/TenantOverrideDependency.cs @@ -0,0 +1,14 @@ +namespace MultitenantExample.AspNetCore; + +/// +/// Registered only for specific tenants, to show the override winning over the +/// container-wide default. +/// +public sealed class TenantOverrideDependency : ITenantDependency +{ + private readonly string _tenantId; + + public TenantOverrideDependency(string tenantId) => _tenantId = tenantId; + + public string Describe() => $"TenantOverrideDependency registered specifically for tenant '{_tenantId}'"; +} diff --git a/src/PoolingExample/ConnectionPoolPolicy.cs b/src/PoolingExample/ConnectionPoolPolicy.cs new file mode 100644 index 0000000..e01f127 --- /dev/null +++ b/src/PoolingExample/ConnectionPoolPolicy.cs @@ -0,0 +1,28 @@ +using Autofac; +using Autofac.Core; +using Autofac.Pooling; + +namespace PoolingExample; + +/// +/// A policy is the hook for deciding how many instances to keep and what to do +/// as they leave and re-enter the pool. +/// +public sealed class ConnectionPoolPolicy : IPooledRegistrationPolicy +{ + public int MaximumRetained => 2; + + public ExpensiveConnection Get(IComponentContext context, IEnumerable parameters, Func getFromPool) + => getFromPool(); + + /// + /// Returning puts the instance back in the pool. + /// Return to discard it instead, which is how you + /// evict an instance that has gone bad. + /// + public bool Return(ExpensiveConnection pooledObject) + { + pooledObject.Reset(); + return true; + } +} diff --git a/src/PoolingExample/ExpensiveConnection.cs b/src/PoolingExample/ExpensiveConnection.cs new file mode 100644 index 0000000..602b1e4 --- /dev/null +++ b/src/PoolingExample/ExpensiveConnection.cs @@ -0,0 +1,37 @@ +namespace PoolingExample; + +/// +/// Stands in for something genuinely costly to construct, which is the only +/// reason to pool anything. +/// +public sealed class ExpensiveConnection : IExpensiveConnection +{ + private static int _created; + + public ExpensiveConnection() + { + Id = Interlocked.Increment(ref _created); + Console.WriteLine($" [constructed connection {Id}]"); + } + + public int Id + { + get; + } + + public int UseCount + { + get; private set; + } + + public static int CreatedCount => _created; + + public void Use() => UseCount++; + + /// + /// Called by the pool policy on the way back into the pool. Pooled objects + /// outlive the scope that used them, so anything request-specific has to be + /// cleared or the next caller inherits it. + /// + public void Reset() => UseCount = 0; +} diff --git a/src/PoolingExample/IExpensiveConnection.cs b/src/PoolingExample/IExpensiveConnection.cs new file mode 100644 index 0000000..a780f7b --- /dev/null +++ b/src/PoolingExample/IExpensiveConnection.cs @@ -0,0 +1,24 @@ +namespace PoolingExample; + +public interface IExpensiveConnection +{ + /// + /// Gets an identifier for this instance, so the example can show when an + /// instance is reused rather than recreated. + /// + int Id + { + get; + } + + /// + /// Gets the number of times this instance has been used since it was last + /// returned to the pool. + /// + int UseCount + { + get; + } + + void Use(); +} diff --git a/src/PoolingExample/PoolingExample.csproj b/src/PoolingExample/PoolingExample.csproj new file mode 100644 index 0000000..7944ca1 --- /dev/null +++ b/src/PoolingExample/PoolingExample.csproj @@ -0,0 +1,14 @@ + + + + Exe + net10.0 + enable + + + + + + + + diff --git a/src/PoolingExample/Program.cs b/src/PoolingExample/Program.cs new file mode 100644 index 0000000..7b8b400 --- /dev/null +++ b/src/PoolingExample/Program.cs @@ -0,0 +1,39 @@ +using Autofac; +using Autofac.Pooling; + +namespace PoolingExample; + +internal static class Program +{ + public static void Main() + { + var builder = new ContainerBuilder(); + builder.RegisterType() + .As() + .PooledInstancePerLifetimeScope(new ConnectionPoolPolicy()); + + using var container = builder.Build(); + + Console.WriteLine("Two scopes one after the other:"); + for (var i = 1; i <= 2; i++) + { + using var scope = container.BeginLifetimeScope(); + var connection = scope.Resolve(); + connection.Use(); + Console.WriteLine($" scope {i} got connection {connection.Id}, use count {connection.UseCount}"); + } + + Console.WriteLine(); + Console.WriteLine("Two scopes open at the same time:"); + using (var first = container.BeginLifetimeScope()) + using (var second = container.BeginLifetimeScope()) + { + Console.WriteLine($" first scope got connection {first.Resolve().Id}"); + Console.WriteLine($" second scope got connection {second.Resolve().Id}"); + } + + Console.WriteLine(); + Console.WriteLine($"Connections constructed in total: {ExpensiveConnection.CreatedCount}"); + Console.WriteLine("Sequential scopes shared one instance; overlapping scopes each needed their own."); + } +} diff --git a/src/PoolingExample/README.md b/src/PoolingExample/README.md new file mode 100644 index 0000000..0628058 --- /dev/null +++ b/src/PoolingExample/README.md @@ -0,0 +1,9 @@ +# PoolingExample + +Reusing expensive instances across lifetime scopes instead of rebuilding them, and a pool policy that resets an instance on its way back into the pool. Watch the constructor log: sequential scopes share one instance, overlapping scopes each get their own. + +Packages: [`Autofac`](https://github.com/autofac/Autofac), [`Autofac.Pooling`](https://github.com/autofac/Autofac.Pooling) + +Run `dotnet run --project src/PoolingExample`. It reports which instance each scope received and how many were constructed in total. + +See [Pooled Instances](https://autofac.readthedocs.io/en/latest/advanced/pooled-instances.html) for the documentation this example follows.