diff --git a/.vscode/launch.json b/.vscode/launch.json
index d5db6fc..97fae70 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -95,6 +95,16 @@
"stopAtEntry": false,
"type": "coreclr"
},
+ {
+ "console": "integratedTerminal",
+ "cwd": "${workspaceFolder}/src/MiddlewarePipelineExample",
+ "name": "MiddlewarePipelineExample",
+ "preLaunchTask": "build",
+ "program": "${workspaceFolder}/src/MiddlewarePipelineExample/bin/Debug/net10.0/MiddlewarePipelineExample.dll",
+ "request": "launch",
+ "stopAtEntry": false,
+ "type": "coreclr"
+ },
{
"cwd": "${workspaceFolder}/src/MultitenantExample.AspNetCore",
"launchSettingsProfile": "MultitenantExample.AspNetCore",
diff --git a/Examples.slnx b/Examples.slnx
index d07123f..664e268 100644
--- a/Examples.slnx
+++ b/Examples.slnx
@@ -13,6 +13,7 @@
+
diff --git a/README.md b/README.md
index 9afac27..4fd6f27 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,7 @@ Each example has its own README with what it shows and how to run it.
| [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) |
+| [MiddlewarePipelineExample](src/MiddlewarePipelineExample/README.md) | Resolve middleware for auditing, timing, caching, ambient state, and scope guards | [`Autofac`](https://github.com/autofac/Autofac) |
### .NET Framework
diff --git a/src/MiddlewarePipelineExample/Middleware/ActivationAuditMiddleware.cs b/src/MiddlewarePipelineExample/Middleware/ActivationAuditMiddleware.cs
new file mode 100644
index 0000000..b24b9c4
--- /dev/null
+++ b/src/MiddlewarePipelineExample/Middleware/ActivationAuditMiddleware.cs
@@ -0,0 +1,29 @@
+using Autofac.Core.Resolving.Pipeline;
+
+namespace MiddlewarePipelineExample.Middleware;
+
+///
+/// Applied to every registration in the container rather than named on each one.
+/// This is the answer to wanting a hook that runs for all registered components
+/// without calling an extension method on each registration in turn.
+///
+public sealed class ActivationAuditMiddleware : IResolveMiddleware
+{
+ private readonly List _log;
+
+ public ActivationAuditMiddleware(List log) => _log = log;
+
+ public PipelinePhase Phase => PipelinePhase.RegistrationPipelineStart;
+
+ public void Execute(ResolveRequestContext context, Action next)
+ {
+ next(context);
+
+ // The instance is available after next() returns, because that is when
+ // the rest of the pipeline has run and activation has happened.
+ if (context.NewInstanceActivated)
+ {
+ _log.Add(context.Instance!.GetType().Name);
+ }
+ }
+}
diff --git a/src/MiddlewarePipelineExample/Middleware/CachingMiddleware.cs b/src/MiddlewarePipelineExample/Middleware/CachingMiddleware.cs
new file mode 100644
index 0000000..01c4bbd
--- /dev/null
+++ b/src/MiddlewarePipelineExample/Middleware/CachingMiddleware.cs
@@ -0,0 +1,33 @@
+using Autofac.Core.Resolving.Pipeline;
+
+namespace MiddlewarePipelineExample.Middleware;
+
+///
+/// Short-circuits the pipeline. Setting
+/// and never calling next means activation is skipped entirely, so nothing
+/// downstream runs. Useful for a cache, and the reason middleware can be cheaper
+/// than a registration that has to construct something to decide it wasn't needed.
+///
+public sealed class CachingMiddleware : IResolveMiddleware
+{
+ private readonly Action _report;
+ private object? _cached;
+
+ public CachingMiddleware(Action report) => _report = report;
+
+ public PipelinePhase Phase => PipelinePhase.ResolveRequestStart;
+
+ public void Execute(ResolveRequestContext context, Action next)
+ {
+ if (_cached is not null)
+ {
+ _report($"cache hit for {context.Service}, activation skipped");
+ context.Instance = _cached;
+ return;
+ }
+
+ next(context);
+ _cached = context.Instance;
+ _report($"cache miss for {context.Service}, instance stored");
+ }
+}
diff --git a/src/MiddlewarePipelineExample/Middleware/CorrelationIdMiddleware.cs b/src/MiddlewarePipelineExample/Middleware/CorrelationIdMiddleware.cs
new file mode 100644
index 0000000..6cb0ada
--- /dev/null
+++ b/src/MiddlewarePipelineExample/Middleware/CorrelationIdMiddleware.cs
@@ -0,0 +1,26 @@
+using Autofac;
+using Autofac.Core.Resolving.Pipeline;
+
+namespace MiddlewarePipelineExample.Middleware;
+
+///
+/// Injects ambient state into a resolve without every caller having to pass it.
+/// The parameter has to be added before the constructor is chosen, which is why
+/// this sits in the parameter selection phase.
+///
+public sealed class CorrelationIdMiddleware : IResolveMiddleware
+{
+ private readonly Func _currentCorrelationId;
+
+ public CorrelationIdMiddleware(Func currentCorrelationId) => _currentCorrelationId = currentCorrelationId;
+
+ public PipelinePhase Phase => PipelinePhase.ParameterSelection;
+
+ public void Execute(ResolveRequestContext context, Action next)
+ {
+ context.ChangeParameters(
+ context.Parameters.Concat([new NamedParameter("correlationId", _currentCorrelationId())]));
+
+ next(context);
+ }
+}
diff --git a/src/MiddlewarePipelineExample/Middleware/RootScopeGuardMiddleware.cs b/src/MiddlewarePipelineExample/Middleware/RootScopeGuardMiddleware.cs
new file mode 100644
index 0000000..41c016b
--- /dev/null
+++ b/src/MiddlewarePipelineExample/Middleware/RootScopeGuardMiddleware.cs
@@ -0,0 +1,26 @@
+using Autofac.Core;
+using Autofac.Core.Lifetime;
+using Autofac.Core.Resolving.Pipeline;
+
+namespace MiddlewarePipelineExample.Middleware;
+
+///
+/// Fails fast with a readable message when a service that expects a unit of work
+/// is resolved straight from the root scope, where it would be captured for the
+/// lifetime of the application.
+///
+public sealed class RootScopeGuardMiddleware : IResolveMiddleware
+{
+ public PipelinePhase Phase => PipelinePhase.ScopeSelection;
+
+ public void Execute(ResolveRequestContext context, Action next)
+ {
+ if (Equals(context.ActivationScope.Tag, LifetimeScope.RootTag))
+ {
+ throw new DependencyResolutionException(
+ $"{context.Service} must be resolved from a child lifetime scope, not the root container.");
+ }
+
+ next(context);
+ }
+}
diff --git a/src/MiddlewarePipelineExample/Middleware/TimingMiddleware.cs b/src/MiddlewarePipelineExample/Middleware/TimingMiddleware.cs
new file mode 100644
index 0000000..655e9d0
--- /dev/null
+++ b/src/MiddlewarePipelineExample/Middleware/TimingMiddleware.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics;
+using Autofac.Core.Resolving.Pipeline;
+
+namespace MiddlewarePipelineExample.Middleware;
+
+///
+/// Measures how long a resolve takes. Attached as service middleware, so it runs
+/// for every resolve of the service regardless of which registration answers it.
+///
+public sealed class TimingMiddleware : IResolveMiddleware
+{
+ private readonly Action _report;
+
+ public TimingMiddleware(Action report) => _report = report;
+
+ public PipelinePhase Phase => PipelinePhase.ResolveRequestStart;
+
+ public void Execute(ResolveRequestContext context, Action next)
+ {
+ var stopwatch = Stopwatch.StartNew();
+ next(context);
+ stopwatch.Stop();
+
+ _report($"{context.Service} took {stopwatch.ElapsedMilliseconds}ms");
+ }
+}
diff --git a/src/MiddlewarePipelineExample/MiddlewarePipelineExample.csproj b/src/MiddlewarePipelineExample/MiddlewarePipelineExample.csproj
new file mode 100644
index 0000000..9d67923
--- /dev/null
+++ b/src/MiddlewarePipelineExample/MiddlewarePipelineExample.csproj
@@ -0,0 +1,13 @@
+
+
+
+ Exe
+ net10.0
+ enable
+
+
+
+
+
+
+
diff --git a/src/MiddlewarePipelineExample/Program.cs b/src/MiddlewarePipelineExample/Program.cs
new file mode 100644
index 0000000..c87605e
--- /dev/null
+++ b/src/MiddlewarePipelineExample/Program.cs
@@ -0,0 +1,166 @@
+using Autofac;
+using Autofac.Core;
+using MiddlewarePipelineExample.Middleware;
+using MiddlewarePipelineExample.Services;
+
+namespace MiddlewarePipelineExample;
+
+internal static class Program
+{
+ ///
+ /// Runs each scenario in turn. They are independent, so they each build their
+ /// own container rather than sharing one.
+ ///
+ public static void Main()
+ {
+ AuditEveryRegistration();
+ TimeAResolve();
+ ShortCircuitWithACache();
+ InjectAmbientState();
+ RejectRootScopeResolves();
+ }
+
+ ///
+ /// Attaches middleware to every registration at once, without naming it on
+ /// each one. Autofac issue #1337 asked for exactly this hook.
+ ///
+ private static void AuditEveryRegistration()
+ {
+ Header("Middleware on every registration");
+
+ var activated = new List();
+ var builder = new ContainerBuilder();
+
+ builder.ComponentRegistryBuilder.Registered += (sender, args) =>
+ args.ComponentRegistration.PipelineBuilding += (sender2, pipeline) =>
+ pipeline.Use(new ActivationAuditMiddleware(activated));
+
+ builder.RegisterType().As();
+ builder.RegisterType().As();
+
+ using var container = builder.Build();
+ using var scope = container.BeginLifetimeScope();
+ scope.Resolve();
+ scope.Resolve();
+
+ Console.WriteLine($" activated: {string.Join(", ", activated)}");
+ Console.WriteLine(" neither registration mentions the middleware.");
+ }
+
+ ///
+ /// Service middleware wraps every resolve of a service, so it can measure what
+ /// activation actually costs without the service or its registration knowing.
+ ///
+ private static void TimeAResolve()
+ {
+ Header("Timing a resolve");
+
+ var builder = new ContainerBuilder();
+ builder.RegisterType().As();
+ builder.RegisterServiceMiddleware(new TimingMiddleware(m => Console.WriteLine($" {m}")));
+
+ using var container = builder.Build();
+ using var scope = container.BeginLifetimeScope();
+ scope.Resolve();
+ }
+
+ ///
+ /// Setting Instance and returning without calling next ends the
+ /// pipeline early, so activation never happens. That is what makes a middleware
+ /// cache cheaper than a registration which has to construct something before it
+ /// can decide the work was unnecessary.
+ ///
+ private static void ShortCircuitWithACache()
+ {
+ Header("Short-circuiting the pipeline");
+
+ var builder = new ContainerBuilder();
+ builder.RegisterType().As();
+ builder.RegisterServiceMiddleware(new CachingMiddleware(m => Console.WriteLine($" {m}")));
+
+ using var container = builder.Build();
+ for (var i = 0; i < 2; i++)
+ {
+ using var scope = container.BeginLifetimeScope();
+ scope.Resolve();
+ }
+
+ Console.WriteLine(" the second resolve never reached activation.");
+ }
+
+ ///
+ /// Supplies a constructor argument no caller passes. Watch which pipeline this
+ /// one attaches to: parameter selection belongs to the registration pipeline, so
+ /// service middleware is rejected outright.
+ ///
+ private static void InjectAmbientState()
+ {
+ Header("Injecting ambient state");
+
+ var correlationId = "req-001";
+ var builder = new ContainerBuilder();
+
+ // Parameter selection is a registration pipeline phase, so this one goes
+ // on the registration. Adding it as service middleware throws, because a
+ // service pipeline has already finished by the time parameters matter.
+ builder.RegisterType()
+ .As()
+ .ConfigurePipeline(pipeline => pipeline.Use(new CorrelationIdMiddleware(() => correlationId)));
+
+ using var container = builder.Build();
+
+ using (var scope = container.BeginLifetimeScope())
+ {
+ Console.WriteLine($" {scope.Resolve().Describe()}");
+ }
+
+ correlationId = "req-002";
+ using (var scope = container.BeginLifetimeScope())
+ {
+ Console.WriteLine($" {scope.Resolve().Describe()}");
+ }
+
+ Console.WriteLine(" no caller passed the correlation id.");
+ }
+
+ ///
+ /// Turns a captured dependency into an immediate, readable failure. Resolving a
+ /// per-scope service straight from the root container would otherwise keep it
+ /// alive for the life of the application, and the symptom usually shows up a
+ /// long way from the cause.
+ ///
+ private static void RejectRootScopeResolves()
+ {
+ Header("Failing fast on a root scope resolve");
+
+ var builder = new ContainerBuilder();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterServiceMiddleware(new RootScopeGuardMiddleware());
+
+ using var container = builder.Build();
+
+ using (var scope = container.BeginLifetimeScope())
+ {
+ Console.WriteLine($" from a child scope: {scope.Resolve().Use()}");
+ }
+
+ try
+ {
+ container.Resolve();
+ }
+ catch (DependencyResolutionException ex)
+ {
+ Console.WriteLine($" from the root scope: {ex.Message.Split(" ---> ")[^1]}");
+ }
+ }
+
+ ///
+ /// Writes a section title so each scenario's output is easy to tell apart.
+ ///
+ private static void Header(string title)
+ {
+ Console.WriteLine();
+ Console.WriteLine(title);
+ Console.WriteLine(new string('-', title.Length));
+ }
+}
diff --git a/src/MiddlewarePipelineExample/README.md b/src/MiddlewarePipelineExample/README.md
new file mode 100644
index 0000000..e5c9315
--- /dev/null
+++ b/src/MiddlewarePipelineExample/README.md
@@ -0,0 +1,11 @@
+# MiddlewarePipelineExample
+
+Five things resolve middleware is actually useful for, each in its own class: auditing every registration without naming the middleware on any of them, timing a resolve, short-circuiting the pipeline from a cache, injecting ambient state as a parameter, and rejecting a resolve that came from the root scope.
+
+The correlation ID scenario is the one worth reading twice. It runs in the parameter selection phase, which belongs to the registration pipeline, so it has to be attached with `ConfigurePipeline` rather than `RegisterServiceMiddleware`. Registering it as service middleware throws, and that error is the clearest explanation of why Autofac has two pipelines rather than one.
+
+Packages: [`Autofac`](https://github.com/autofac/Autofac)
+
+Run `dotnet run --project src/MiddlewarePipelineExample`. Each scenario prints a short section showing what the middleware did.
+
+See [Resolve Pipelines](https://autofac.readthedocs.io/en/latest/advanced/pipelines.html) for the documentation this example follows.
diff --git a/src/MiddlewarePipelineExample/Services/IReportService.cs b/src/MiddlewarePipelineExample/Services/IReportService.cs
new file mode 100644
index 0000000..d245a9b
--- /dev/null
+++ b/src/MiddlewarePipelineExample/Services/IReportService.cs
@@ -0,0 +1,6 @@
+namespace MiddlewarePipelineExample.Services;
+
+public interface IReportService
+{
+ string Run();
+}
diff --git a/src/MiddlewarePipelineExample/Services/IRequestHandler.cs b/src/MiddlewarePipelineExample/Services/IRequestHandler.cs
new file mode 100644
index 0000000..ffb0609
--- /dev/null
+++ b/src/MiddlewarePipelineExample/Services/IRequestHandler.cs
@@ -0,0 +1,6 @@
+namespace MiddlewarePipelineExample.Services;
+
+public interface IRequestHandler
+{
+ string Describe();
+}
diff --git a/src/MiddlewarePipelineExample/Services/IScopedResource.cs b/src/MiddlewarePipelineExample/Services/IScopedResource.cs
new file mode 100644
index 0000000..9334f57
--- /dev/null
+++ b/src/MiddlewarePipelineExample/Services/IScopedResource.cs
@@ -0,0 +1,6 @@
+namespace MiddlewarePipelineExample.Services;
+
+public interface IScopedResource
+{
+ string Use();
+}
diff --git a/src/MiddlewarePipelineExample/Services/ISlowService.cs b/src/MiddlewarePipelineExample/Services/ISlowService.cs
new file mode 100644
index 0000000..ee69687
--- /dev/null
+++ b/src/MiddlewarePipelineExample/Services/ISlowService.cs
@@ -0,0 +1,6 @@
+namespace MiddlewarePipelineExample.Services;
+
+public interface ISlowService
+{
+ string Fetch();
+}
diff --git a/src/MiddlewarePipelineExample/Services/ReportService.cs b/src/MiddlewarePipelineExample/Services/ReportService.cs
new file mode 100644
index 0000000..e5ba61f
--- /dev/null
+++ b/src/MiddlewarePipelineExample/Services/ReportService.cs
@@ -0,0 +1,6 @@
+namespace MiddlewarePipelineExample.Services;
+
+public sealed class ReportService : IReportService
+{
+ public string Run() => "report";
+}
diff --git a/src/MiddlewarePipelineExample/Services/RequestHandler.cs b/src/MiddlewarePipelineExample/Services/RequestHandler.cs
new file mode 100644
index 0000000..ea3165b
--- /dev/null
+++ b/src/MiddlewarePipelineExample/Services/RequestHandler.cs
@@ -0,0 +1,14 @@
+namespace MiddlewarePipelineExample.Services;
+
+///
+/// Takes a correlation identifier it has no way to know at registration time.
+/// Middleware supplies it per resolve.
+///
+public sealed class RequestHandler : IRequestHandler
+{
+ private readonly string _correlationId;
+
+ public RequestHandler(string correlationId) => _correlationId = correlationId;
+
+ public string Describe() => $"handling request {_correlationId}";
+}
diff --git a/src/MiddlewarePipelineExample/Services/ScopedResource.cs b/src/MiddlewarePipelineExample/Services/ScopedResource.cs
new file mode 100644
index 0000000..92f892b
--- /dev/null
+++ b/src/MiddlewarePipelineExample/Services/ScopedResource.cs
@@ -0,0 +1,6 @@
+namespace MiddlewarePipelineExample.Services;
+
+public sealed class ScopedResource : IScopedResource
+{
+ public string Use() => "scoped work";
+}
diff --git a/src/MiddlewarePipelineExample/Services/SlowService.cs b/src/MiddlewarePipelineExample/Services/SlowService.cs
new file mode 100644
index 0000000..74ed47a
--- /dev/null
+++ b/src/MiddlewarePipelineExample/Services/SlowService.cs
@@ -0,0 +1,12 @@
+namespace MiddlewarePipelineExample.Services;
+
+///
+/// Expensive to construct, which is what makes the timing and caching middleware
+/// worth looking at.
+///
+public sealed class SlowService : ISlowService
+{
+ public SlowService() => Thread.Sleep(75);
+
+ public string Fetch() => "data";
+}