From 8512bf7ca5a4065cd6f1c76dca4875965a3121cd Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Tue, 1 Sep 2026 11:43:40 -0700 Subject: [PATCH 1/3] Add examples for DynamicProxy, Pooling, DotGraph, and multitenant ASP.NET Core Four shipping packages had no example at all, which left the documentation as the only reference for them. DynamicProxyExample shows interception wired both ways, on the registration and by attribute on the contract, because which one to reach for is the question people actually have. PoolingExample leans on its own output: sequential scopes share one instance while overlapping scopes each construct their own, and the use count printed after reuse proves the policy reset the instance on its way back to the pool. That distinction is the whole difference between pooling and a singleton. DotGraphExample includes a decorator so the captured graph has an adapter chain worth looking at, rather than a flat list nobody needs a picture of. AspNetCoreMultitenantExample writes its own ITenantIdentificationStrategy, since the package deliberately ships none, and covers the case people get wrong: a tenant with no override falls back to the container default rather than failing. Part of #32 --- .vscode/launch.json | 45 ++++++++++++++++ Examples.slnx | 4 ++ README.md | 4 ++ .../AspNetCoreMultitenantExample.csproj | 13 +++++ .../Controllers/TenantController.cs | 30 +++++++++++ .../DefaultDependency.cs | 9 ++++ .../ITenantDependency.cs | 6 +++ .../MultitenantContainerSetup.cs | 31 +++++++++++ src/AspNetCoreMultitenantExample/Program.cs | 17 ++++++ .../Properties/launchSettings.json | 11 ++++ ...QueryStringTenantIdentificationStrategy.cs | 24 +++++++++ src/AspNetCoreMultitenantExample/README.md | 9 ++++ src/AspNetCoreMultitenantExample/Startup.cs | 37 +++++++++++++ .../TenantOverrideDependency.cs | 14 +++++ src/DotGraphExample/DotGraphExample.csproj | 14 +++++ src/DotGraphExample/Program.cs | 42 +++++++++++++++ src/DotGraphExample/README.md | 9 ++++ src/DotGraphExample/Services.cs | 54 +++++++++++++++++++ src/DynamicProxyExample/Calculator.cs | 6 +++ src/DynamicProxyExample/CallLogger.cs | 22 ++++++++ .../DynamicProxyExample.csproj | 14 +++++ src/DynamicProxyExample/Greeter.cs | 6 +++ src/DynamicProxyExample/ICalculator.cs | 10 ++++ src/DynamicProxyExample/IGreeter.cs | 13 +++++ src/DynamicProxyExample/Program.cs | 39 ++++++++++++++ src/DynamicProxyExample/README.md | 9 ++++ src/PoolingExample/ConnectionPoolPolicy.cs | 28 ++++++++++ src/PoolingExample/ExpensiveConnection.cs | 37 +++++++++++++ src/PoolingExample/IExpensiveConnection.cs | 24 +++++++++ src/PoolingExample/PoolingExample.csproj | 14 +++++ src/PoolingExample/Program.cs | 39 ++++++++++++++ src/PoolingExample/README.md | 9 ++++ 32 files changed, 643 insertions(+) create mode 100644 src/AspNetCoreMultitenantExample/AspNetCoreMultitenantExample.csproj create mode 100644 src/AspNetCoreMultitenantExample/Controllers/TenantController.cs create mode 100644 src/AspNetCoreMultitenantExample/DefaultDependency.cs create mode 100644 src/AspNetCoreMultitenantExample/ITenantDependency.cs create mode 100644 src/AspNetCoreMultitenantExample/MultitenantContainerSetup.cs create mode 100644 src/AspNetCoreMultitenantExample/Program.cs create mode 100644 src/AspNetCoreMultitenantExample/Properties/launchSettings.json create mode 100644 src/AspNetCoreMultitenantExample/QueryStringTenantIdentificationStrategy.cs create mode 100644 src/AspNetCoreMultitenantExample/README.md create mode 100644 src/AspNetCoreMultitenantExample/Startup.cs create mode 100644 src/AspNetCoreMultitenantExample/TenantOverrideDependency.cs create mode 100644 src/DotGraphExample/DotGraphExample.csproj create mode 100644 src/DotGraphExample/Program.cs create mode 100644 src/DotGraphExample/README.md create mode 100644 src/DotGraphExample/Services.cs create mode 100644 src/DynamicProxyExample/Calculator.cs create mode 100644 src/DynamicProxyExample/CallLogger.cs create mode 100644 src/DynamicProxyExample/DynamicProxyExample.csproj create mode 100644 src/DynamicProxyExample/Greeter.cs create mode 100644 src/DynamicProxyExample/ICalculator.cs create mode 100644 src/DynamicProxyExample/IGreeter.cs create mode 100644 src/DynamicProxyExample/Program.cs create mode 100644 src/DynamicProxyExample/README.md create mode 100644 src/PoolingExample/ConnectionPoolPolicy.cs create mode 100644 src/PoolingExample/ExpensiveConnection.cs create mode 100644 src/PoolingExample/IExpensiveConnection.cs create mode 100644 src/PoolingExample/PoolingExample.csproj create mode 100644 src/PoolingExample/Program.cs create mode 100644 src/PoolingExample/README.md diff --git a/.vscode/launch.json b/.vscode/launch.json index f1f259a..f50c6d6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -30,6 +30,21 @@ "stopAtEntry": false, "type": "coreclr" }, + { + "cwd": "${workspaceFolder}/src/AspNetCoreMultitenantExample", + "launchSettingsProfile": "AspNetCoreMultitenantExample", + "name": "AspNetCoreMultitenantExample", + "preLaunchTask": "build", + "program": "${workspaceFolder}/src/AspNetCoreMultitenantExample/bin/Debug/net10.0/AspNetCoreMultitenantExample.dll", + "request": "launch", + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\\\bNow listening on:\\\\s+(https?://\\\\S+)", + "uriFormat": "%s/api/tenant?tenant=alpha" + }, + "stopAtEntry": false, + "type": "coreclr" + }, { "cwd": "${workspaceFolder}/src/AspNetCoreNoStartupExample", "launchSettingsProfile": "AspNetCoreNoStartupExample", @@ -65,6 +80,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", @@ -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..013f5f3 100644 --- a/Examples.slnx +++ b/Examples.slnx @@ -2,6 +2,7 @@ + @@ -9,8 +10,11 @@ + + + diff --git a/README.md b/README.md index 4cbad45..7bcd047 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Each example has its own README with what it shows and how to run it. | Example | Demonstrates | Packages | | --- | --- | --- | | [AspNetCoreExample](src/AspNetCoreExample/README.md) | A `Startup` class whose `ConfigureContainer` takes a `ContainerBuilder` | [`Autofac.Extensions.DependencyInjection`](https://github.com/autofac/Autofac.Extensions.DependencyInjection) | +| [AspNetCoreMultitenantExample](src/AspNetCoreMultitenantExample/README.md) | Per-tenant registration overrides, with the tenant read from the query string | [`Autofac.AspNetCore.Multitenant`](https://github.com/autofac/Autofac.AspNetCore.Multitenant) | | [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) | @@ -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/AspNetCoreMultitenantExample/AspNetCoreMultitenantExample.csproj b/src/AspNetCoreMultitenantExample/AspNetCoreMultitenantExample.csproj new file mode 100644 index 0000000..6230502 --- /dev/null +++ b/src/AspNetCoreMultitenantExample/AspNetCoreMultitenantExample.csproj @@ -0,0 +1,13 @@ + + + + ../../build/AspNet.ruleset + net10.0 + enable + + + + + + + diff --git a/src/AspNetCoreMultitenantExample/Controllers/TenantController.cs b/src/AspNetCoreMultitenantExample/Controllers/TenantController.cs new file mode 100644 index 0000000..857149d --- /dev/null +++ b/src/AspNetCoreMultitenantExample/Controllers/TenantController.cs @@ -0,0 +1,30 @@ +using Autofac.Multitenant; +using Microsoft.AspNetCore.Mvc; + +namespace AspNetCoreMultitenantExample.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/AspNetCoreMultitenantExample/DefaultDependency.cs b/src/AspNetCoreMultitenantExample/DefaultDependency.cs new file mode 100644 index 0000000..478db1c --- /dev/null +++ b/src/AspNetCoreMultitenantExample/DefaultDependency.cs @@ -0,0 +1,9 @@ +namespace AspNetCoreMultitenantExample; + +/// +/// 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/AspNetCoreMultitenantExample/ITenantDependency.cs b/src/AspNetCoreMultitenantExample/ITenantDependency.cs new file mode 100644 index 0000000..138528c --- /dev/null +++ b/src/AspNetCoreMultitenantExample/ITenantDependency.cs @@ -0,0 +1,6 @@ +namespace AspNetCoreMultitenantExample; + +public interface ITenantDependency +{ + string Describe(); +} diff --git a/src/AspNetCoreMultitenantExample/MultitenantContainerSetup.cs b/src/AspNetCoreMultitenantExample/MultitenantContainerSetup.cs new file mode 100644 index 0000000..b0580cf --- /dev/null +++ b/src/AspNetCoreMultitenantExample/MultitenantContainerSetup.cs @@ -0,0 +1,31 @@ +using Autofac; +using Autofac.Multitenant; + +namespace AspNetCoreMultitenantExample; + +/// +/// 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/AspNetCoreMultitenantExample/Program.cs b/src/AspNetCoreMultitenantExample/Program.cs new file mode 100644 index 0000000..b885a53 --- /dev/null +++ b/src/AspNetCoreMultitenantExample/Program.cs @@ -0,0 +1,17 @@ +namespace AspNetCoreMultitenantExample; + +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/AspNetCoreMultitenantExample/Properties/launchSettings.json b/src/AspNetCoreMultitenantExample/Properties/launchSettings.json new file mode 100644 index 0000000..4d88fa5 --- /dev/null +++ b/src/AspNetCoreMultitenantExample/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "AspNetCoreMultitenantExample": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + }, + "launchBrowser": false + } + } +} diff --git a/src/AspNetCoreMultitenantExample/QueryStringTenantIdentificationStrategy.cs b/src/AspNetCoreMultitenantExample/QueryStringTenantIdentificationStrategy.cs new file mode 100644 index 0000000..0acf13d --- /dev/null +++ b/src/AspNetCoreMultitenantExample/QueryStringTenantIdentificationStrategy.cs @@ -0,0 +1,24 @@ +using Autofac.Multitenant; + +namespace AspNetCoreMultitenantExample; + +/// +/// 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/AspNetCoreMultitenantExample/README.md b/src/AspNetCoreMultitenantExample/README.md new file mode 100644 index 0000000..5f21cdd --- /dev/null +++ b/src/AspNetCoreMultitenantExample/README.md @@ -0,0 +1,9 @@ +# AspNetCoreMultitenantExample + +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/AspNetCoreMultitenantExample`, 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/AspNetCoreMultitenantExample/Startup.cs b/src/AspNetCoreMultitenantExample/Startup.cs new file mode 100644 index 0000000..ca8dd13 --- /dev/null +++ b/src/AspNetCoreMultitenantExample/Startup.cs @@ -0,0 +1,37 @@ +using Autofac; +using Autofac.Multitenant; + +namespace AspNetCoreMultitenantExample; + +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/AspNetCoreMultitenantExample/TenantOverrideDependency.cs b/src/AspNetCoreMultitenantExample/TenantOverrideDependency.cs new file mode 100644 index 0000000..b42adaa --- /dev/null +++ b/src/AspNetCoreMultitenantExample/TenantOverrideDependency.cs @@ -0,0 +1,14 @@ +namespace AspNetCoreMultitenantExample; + +/// +/// 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/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/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/Services.cs b/src/DotGraphExample/Services.cs new file mode 100644 index 0000000..85450c9 --- /dev/null +++ b/src/DotGraphExample/Services.cs @@ -0,0 +1,54 @@ +namespace DotGraphExample; + +// A graph is only interesting if there is something to draw, so these services +// nest a couple of levels deep and include a decorator and a keyed service. +public interface IDataSource +{ + string Read(); +} + +public interface IFormatter +{ + string Format(string value); +} + +public interface IReportGenerator +{ + string Generate(); +} + +public sealed class DataSource : IDataSource +{ + public string Read() => "42"; +} + +public sealed class UppercaseFormatter : IFormatter +{ + public string Format(string value) => value.ToUpperInvariant(); +} + +/// +/// A decorator, so the graph shows the adapter chain rather than a flat list. +/// +public sealed class BracketFormatter : IFormatter +{ + private readonly IFormatter _inner; + + public BracketFormatter(IFormatter inner) => _inner = inner; + + public string Format(string value) => $"[{_inner.Format(value)}]"; +} + +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/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/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. From 290426ee73c5fdaffde1104e66d363e30c79878c Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Tue, 1 Sep 2026 11:51:32 -0700 Subject: [PATCH 2/3] Rename AspNetCoreMultitenantExample to MultitenantExample.AspNetCore Matches the MultitenantExample.ConsoleApplication, .MvcApplication and .WcfService naming already used for the other multitenant samples, so all four sort together. Namespaces follow the project name, as they do in the sibling projects. It stays in the ASP.NET Core solution folder and the ASP.NET Core section of the README, since that is where someone looking for ASP.NET Core would go first. Part of #32 --- .vscode/launch.json | 8 ++++---- Examples.slnx | 2 +- README.md | 2 +- .../Controllers/TenantController.cs | 2 +- .../DefaultDependency.cs | 2 +- .../ITenantDependency.cs | 2 +- .../MultitenantContainerSetup.cs | 2 +- .../MultitenantExample.AspNetCore.csproj} | 0 .../Program.cs | 2 +- .../Properties/launchSettings.json | 2 +- .../QueryStringTenantIdentificationStrategy.cs | 2 +- .../README.md | 4 ++-- .../Startup.cs | 2 +- .../TenantOverrideDependency.cs | 2 +- 14 files changed, 17 insertions(+), 17 deletions(-) rename src/{AspNetCoreMultitenantExample => MultitenantExample.AspNetCore}/Controllers/TenantController.cs (93%) rename src/{AspNetCoreMultitenantExample => MultitenantExample.AspNetCore}/DefaultDependency.cs (84%) rename src/{AspNetCoreMultitenantExample => MultitenantExample.AspNetCore}/ITenantDependency.cs (58%) rename src/{AspNetCoreMultitenantExample => MultitenantExample.AspNetCore}/MultitenantContainerSetup.cs (96%) rename src/{AspNetCoreMultitenantExample/AspNetCoreMultitenantExample.csproj => MultitenantExample.AspNetCore/MultitenantExample.AspNetCore.csproj} (100%) rename src/{AspNetCoreMultitenantExample => MultitenantExample.AspNetCore}/Program.cs (93%) rename src/{AspNetCoreMultitenantExample => MultitenantExample.AspNetCore}/Properties/launchSettings.json (81%) rename src/{AspNetCoreMultitenantExample => MultitenantExample.AspNetCore}/QueryStringTenantIdentificationStrategy.cs (95%) rename src/{AspNetCoreMultitenantExample => MultitenantExample.AspNetCore}/README.md (66%) rename src/{AspNetCoreMultitenantExample => MultitenantExample.AspNetCore}/Startup.cs (96%) rename src/{AspNetCoreMultitenantExample => MultitenantExample.AspNetCore}/TenantOverrideDependency.cs (90%) diff --git a/.vscode/launch.json b/.vscode/launch.json index f50c6d6..16c71f1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -31,11 +31,11 @@ "type": "coreclr" }, { - "cwd": "${workspaceFolder}/src/AspNetCoreMultitenantExample", - "launchSettingsProfile": "AspNetCoreMultitenantExample", - "name": "AspNetCoreMultitenantExample", + "cwd": "${workspaceFolder}/src/MultitenantExample.AspNetCore", + "launchSettingsProfile": "MultitenantExample.AspNetCore", + "name": "MultitenantExample.AspNetCore", "preLaunchTask": "build", - "program": "${workspaceFolder}/src/AspNetCoreMultitenantExample/bin/Debug/net10.0/AspNetCoreMultitenantExample.dll", + "program": "${workspaceFolder}/src/MultitenantExample.AspNetCore/bin/Debug/net10.0/MultitenantExample.AspNetCore.dll", "request": "launch", "serverReadyAction": { "action": "openExternally", diff --git a/Examples.slnx b/Examples.slnx index 013f5f3..d07123f 100644 --- a/Examples.slnx +++ b/Examples.slnx @@ -2,8 +2,8 @@ - + diff --git a/README.md b/README.md index 7bcd047..9afac27 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ Each example has its own README with what it shows and how to run it. | Example | Demonstrates | Packages | | --- | --- | --- | | [AspNetCoreExample](src/AspNetCoreExample/README.md) | A `Startup` class whose `ConfigureContainer` takes a `ContainerBuilder` | [`Autofac.Extensions.DependencyInjection`](https://github.com/autofac/Autofac.Extensions.DependencyInjection) | -| [AspNetCoreMultitenantExample](src/AspNetCoreMultitenantExample/README.md) | Per-tenant registration overrides, with the tenant read from the query string | [`Autofac.AspNetCore.Multitenant`](https://github.com/autofac/Autofac.AspNetCore.Multitenant) | | [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 diff --git a/src/AspNetCoreMultitenantExample/Controllers/TenantController.cs b/src/MultitenantExample.AspNetCore/Controllers/TenantController.cs similarity index 93% rename from src/AspNetCoreMultitenantExample/Controllers/TenantController.cs rename to src/MultitenantExample.AspNetCore/Controllers/TenantController.cs index 857149d..ef1d26d 100644 --- a/src/AspNetCoreMultitenantExample/Controllers/TenantController.cs +++ b/src/MultitenantExample.AspNetCore/Controllers/TenantController.cs @@ -1,7 +1,7 @@ using Autofac.Multitenant; using Microsoft.AspNetCore.Mvc; -namespace AspNetCoreMultitenantExample.Controllers; +namespace MultitenantExample.AspNetCore.Controllers; [ApiController] [Route("api/[controller]")] diff --git a/src/AspNetCoreMultitenantExample/DefaultDependency.cs b/src/MultitenantExample.AspNetCore/DefaultDependency.cs similarity index 84% rename from src/AspNetCoreMultitenantExample/DefaultDependency.cs rename to src/MultitenantExample.AspNetCore/DefaultDependency.cs index 478db1c..94a1780 100644 --- a/src/AspNetCoreMultitenantExample/DefaultDependency.cs +++ b/src/MultitenantExample.AspNetCore/DefaultDependency.cs @@ -1,4 +1,4 @@ -namespace AspNetCoreMultitenantExample; +namespace MultitenantExample.AspNetCore; /// /// What every tenant gets unless it registers an override. diff --git a/src/AspNetCoreMultitenantExample/ITenantDependency.cs b/src/MultitenantExample.AspNetCore/ITenantDependency.cs similarity index 58% rename from src/AspNetCoreMultitenantExample/ITenantDependency.cs rename to src/MultitenantExample.AspNetCore/ITenantDependency.cs index 138528c..e3c846a 100644 --- a/src/AspNetCoreMultitenantExample/ITenantDependency.cs +++ b/src/MultitenantExample.AspNetCore/ITenantDependency.cs @@ -1,4 +1,4 @@ -namespace AspNetCoreMultitenantExample; +namespace MultitenantExample.AspNetCore; public interface ITenantDependency { diff --git a/src/AspNetCoreMultitenantExample/MultitenantContainerSetup.cs b/src/MultitenantExample.AspNetCore/MultitenantContainerSetup.cs similarity index 96% rename from src/AspNetCoreMultitenantExample/MultitenantContainerSetup.cs rename to src/MultitenantExample.AspNetCore/MultitenantContainerSetup.cs index b0580cf..0df6bff 100644 --- a/src/AspNetCoreMultitenantExample/MultitenantContainerSetup.cs +++ b/src/MultitenantExample.AspNetCore/MultitenantContainerSetup.cs @@ -1,7 +1,7 @@ using Autofac; using Autofac.Multitenant; -namespace AspNetCoreMultitenantExample; +namespace MultitenantExample.AspNetCore; /// /// Tenant overrides are configured here rather than in Startup, because diff --git a/src/AspNetCoreMultitenantExample/AspNetCoreMultitenantExample.csproj b/src/MultitenantExample.AspNetCore/MultitenantExample.AspNetCore.csproj similarity index 100% rename from src/AspNetCoreMultitenantExample/AspNetCoreMultitenantExample.csproj rename to src/MultitenantExample.AspNetCore/MultitenantExample.AspNetCore.csproj diff --git a/src/AspNetCoreMultitenantExample/Program.cs b/src/MultitenantExample.AspNetCore/Program.cs similarity index 93% rename from src/AspNetCoreMultitenantExample/Program.cs rename to src/MultitenantExample.AspNetCore/Program.cs index b885a53..b844b2a 100644 --- a/src/AspNetCoreMultitenantExample/Program.cs +++ b/src/MultitenantExample.AspNetCore/Program.cs @@ -1,4 +1,4 @@ -namespace AspNetCoreMultitenantExample; +namespace MultitenantExample.AspNetCore; public static class Program { diff --git a/src/AspNetCoreMultitenantExample/Properties/launchSettings.json b/src/MultitenantExample.AspNetCore/Properties/launchSettings.json similarity index 81% rename from src/AspNetCoreMultitenantExample/Properties/launchSettings.json rename to src/MultitenantExample.AspNetCore/Properties/launchSettings.json index 4d88fa5..ca516a9 100644 --- a/src/AspNetCoreMultitenantExample/Properties/launchSettings.json +++ b/src/MultitenantExample.AspNetCore/Properties/launchSettings.json @@ -1,6 +1,6 @@ { "profiles": { - "AspNetCoreMultitenantExample": { + "MultitenantExample.AspNetCore": { "commandName": "Project", "environmentVariables": { "DOTNET_ENVIRONMENT": "Development" diff --git a/src/AspNetCoreMultitenantExample/QueryStringTenantIdentificationStrategy.cs b/src/MultitenantExample.AspNetCore/QueryStringTenantIdentificationStrategy.cs similarity index 95% rename from src/AspNetCoreMultitenantExample/QueryStringTenantIdentificationStrategy.cs rename to src/MultitenantExample.AspNetCore/QueryStringTenantIdentificationStrategy.cs index 0acf13d..9860b42 100644 --- a/src/AspNetCoreMultitenantExample/QueryStringTenantIdentificationStrategy.cs +++ b/src/MultitenantExample.AspNetCore/QueryStringTenantIdentificationStrategy.cs @@ -1,6 +1,6 @@ using Autofac.Multitenant; -namespace AspNetCoreMultitenantExample; +namespace MultitenantExample.AspNetCore; /// /// Identifies the tenant from a ?tenant= query string value. A real diff --git a/src/AspNetCoreMultitenantExample/README.md b/src/MultitenantExample.AspNetCore/README.md similarity index 66% rename from src/AspNetCoreMultitenantExample/README.md rename to src/MultitenantExample.AspNetCore/README.md index 5f21cdd..26c7bcd 100644 --- a/src/AspNetCoreMultitenantExample/README.md +++ b/src/MultitenantExample.AspNetCore/README.md @@ -1,9 +1,9 @@ -# AspNetCoreMultitenantExample +# 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/AspNetCoreMultitenantExample`, then compare , , and . +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/AspNetCoreMultitenantExample/Startup.cs b/src/MultitenantExample.AspNetCore/Startup.cs similarity index 96% rename from src/AspNetCoreMultitenantExample/Startup.cs rename to src/MultitenantExample.AspNetCore/Startup.cs index ca8dd13..bb8ca71 100644 --- a/src/AspNetCoreMultitenantExample/Startup.cs +++ b/src/MultitenantExample.AspNetCore/Startup.cs @@ -1,7 +1,7 @@ using Autofac; using Autofac.Multitenant; -namespace AspNetCoreMultitenantExample; +namespace MultitenantExample.AspNetCore; public sealed class Startup { diff --git a/src/AspNetCoreMultitenantExample/TenantOverrideDependency.cs b/src/MultitenantExample.AspNetCore/TenantOverrideDependency.cs similarity index 90% rename from src/AspNetCoreMultitenantExample/TenantOverrideDependency.cs rename to src/MultitenantExample.AspNetCore/TenantOverrideDependency.cs index b42adaa..4c5cc83 100644 --- a/src/AspNetCoreMultitenantExample/TenantOverrideDependency.cs +++ b/src/MultitenantExample.AspNetCore/TenantOverrideDependency.cs @@ -1,4 +1,4 @@ -namespace AspNetCoreMultitenantExample; +namespace MultitenantExample.AspNetCore; /// /// Registered only for specific tenants, to show the override winning over the From d643a7227128eb1b217505dc3a1075a4af97be27 Mon Sep 17 00:00:00 2001 From: Travis Illig Date: Tue, 1 Sep 2026 11:55:55 -0700 Subject: [PATCH 3/3] Sort launch configurations and split the DotGraph services into one file each The rename in the previous commit left MultitenantExample.AspNetCore sitting in the alphabetical slot its old name occupied. DotGraphExample kept six types in a single Services.cs, which does not match the rest of the repository or the two sibling examples added alongside it. Split with no behaviour change: the captured graph renders byte for byte identically. Part of #32 --- .vscode/launch.json | 30 ++++++------- src/DotGraphExample/BracketFormatter.cs | 14 ++++++ src/DotGraphExample/DataSource.cs | 6 +++ src/DotGraphExample/IDataSource.cs | 6 +++ src/DotGraphExample/IFormatter.cs | 6 +++ src/DotGraphExample/IReportGenerator.cs | 6 +++ src/DotGraphExample/ReportGenerator.cs | 19 ++++++++ src/DotGraphExample/Services.cs | 54 ----------------------- src/DotGraphExample/UppercaseFormatter.cs | 6 +++ 9 files changed, 78 insertions(+), 69 deletions(-) create mode 100644 src/DotGraphExample/BracketFormatter.cs create mode 100644 src/DotGraphExample/DataSource.cs create mode 100644 src/DotGraphExample/IDataSource.cs create mode 100644 src/DotGraphExample/IFormatter.cs create mode 100644 src/DotGraphExample/IReportGenerator.cs create mode 100644 src/DotGraphExample/ReportGenerator.cs delete mode 100644 src/DotGraphExample/Services.cs create mode 100644 src/DotGraphExample/UppercaseFormatter.cs diff --git a/.vscode/launch.json b/.vscode/launch.json index 16c71f1..d5db6fc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -30,21 +30,6 @@ "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" - }, { "cwd": "${workspaceFolder}/src/AspNetCoreNoStartupExample", "launchSettingsProfile": "AspNetCoreNoStartupExample", @@ -110,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", 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/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/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/Services.cs b/src/DotGraphExample/Services.cs deleted file mode 100644 index 85450c9..0000000 --- a/src/DotGraphExample/Services.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace DotGraphExample; - -// A graph is only interesting if there is something to draw, so these services -// nest a couple of levels deep and include a decorator and a keyed service. -public interface IDataSource -{ - string Read(); -} - -public interface IFormatter -{ - string Format(string value); -} - -public interface IReportGenerator -{ - string Generate(); -} - -public sealed class DataSource : IDataSource -{ - public string Read() => "42"; -} - -public sealed class UppercaseFormatter : IFormatter -{ - public string Format(string value) => value.ToUpperInvariant(); -} - -/// -/// A decorator, so the graph shows the adapter chain rather than a flat list. -/// -public sealed class BracketFormatter : IFormatter -{ - private readonly IFormatter _inner; - - public BracketFormatter(IFormatter inner) => _inner = inner; - - public string Format(string value) => $"[{_inner.Format(value)}]"; -} - -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(); +}