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.