diff --git a/.editorconfig b/.editorconfig
index 1ffed87a..da3c3836 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -109,12 +109,13 @@ dotnet_naming_symbols.static_fields.required_modifiers = static
dotnet_naming_style.static_field_style.capitalization = camel_case
dotnet_naming_style.static_field_style.required_prefix = s_
-# Instance fields are camelCase and start with _
-dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
-dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
-dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
+# Non-public instance fields are camelCase and start with _
+dotnet_naming_rule.non_public_instance_fields_should_be_camel_case.severity = suggestion
+dotnet_naming_rule.non_public_instance_fields_should_be_camel_case.symbols = non_public_instance_fields
+dotnet_naming_rule.non_public_instance_fields_should_be_camel_case.style = instance_field_style
-dotnet_naming_symbols.instance_fields.applicable_kinds = field
+dotnet_naming_symbols.non_public_instance_fields.applicable_kinds = field
+dotnet_naming_symbols.non_public_instance_fields.applicable_accessibilities = protected, internal, protected_internal, private_protected, private
dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _
@@ -226,10 +227,18 @@ csharp_preserve_single_line_statements = true
# Custom bits
##########################################
-# Always have accessibility keyword
+# IDE0007: Use var instead of explicit type
+dotnet_diagnostic.IDE0007.severity = warning
+csharp_style_var_for_built_in_types = true:warning
+csharp_style_var_when_type_is_apparent = true:warning
+csharp_style_var_elsewhere = true:warning
+
+# IDE0040: Add accessibility modifiers
+dotnet_diagnostic.IDE0040.severity = warning
dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
-# Use file scoped namespaces
+# IDE0161: Use file-scoped namespace
+dotnet_diagnostic.IDE0161.severity = warning
csharp_style_namespace_declarations = file_scoped:warning
# CA1019: Define accessors for attribute arguments
@@ -237,6 +246,7 @@ dotnet_diagnostic.CA1019.severity = none
# IDE0305: Simplify collection initialization
dotnet_diagnostic.IDE0305.severity = none
+dotnet_style_prefer_collection_expression = false
# Verify settings
[*.{received,verified}.{cs,txt}]
diff --git a/AutoCtor.slnx b/AutoCtor.slnx
index 27fdb8c1..fc96fc2f 100644
--- a/AutoCtor.slnx
+++ b/AutoCtor.slnx
@@ -1,5 +1,6 @@
+
@@ -30,7 +31,9 @@
-
+
+
+
diff --git a/readme.md b/readme.md
index 06691225..7e8b4f65 100644
--- a/readme.md
+++ b/readme.md
@@ -50,6 +50,7 @@ public partial class AService
* [NuGet packages](#nuget-packages)
* [Code Fixes (new in 3.0)](#code-fixes-new-in-30)
+ * [Service Provider Source Generator (new in 4.0)](#service-provider-source-generator-new-in-40)
* [Examples](#examples)
* [Basic](#basic)
* [Inherited](#inherited)
@@ -60,11 +61,15 @@ public partial class AService
* [Optional Parameters](#optional-parameters)
* [Argument Guards](#argument-guards)
* [Keyed Services](#keyed-services)
+ * [Service Provider](#service-provider)
+ * [Scanning](#scanning)
+ * [Keyed Services](#keyed-services-1)
+ * [Factories](#factories)
+ * [Falling back to another provider](#falling-back-to-another-provider)
+ * [Modules](#modules)
* [Other](#other)
* [Embedding The Attributes](#embedding-the-attributes)
- * [Keeping Attributes In Code](#keeping-attributes-in-code)
- * [Star History](#star-history)
- * [Stats](#stats)
+ * [Keeping Attributes In Code](#keeping-attributes-in-code)
## NuGet packages
@@ -76,6 +81,10 @@ Starting in version 3.0, a new diagnostic has been introduced: `ACTR007 Use [Aut
In 3.1 another code fix was added: `Add [AutoConstruct] to type`. This will add `[AutoConstruct]` to a type with no constructor and relevant fields. This is useful when working on a new type.
+## Service Provider Source Generator (new in 4.0)
+
+In version 4.0 you can now generate your own Service Provider that will work with the constructors generated by AutoCtor. See [Service Provider](#service-provider)
+
## Examples
### Basic
@@ -107,17 +116,14 @@ public partial class Basic
partial class Basic
{
- [global::System.Runtime.CompilerServices.CompilerGenerated]
[global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCode]
- [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public Basic(global::IService service)
{
this._service = service;
}
}
```
-snippet source | anchor
+snippet source | anchor
@@ -160,10 +166,7 @@ public partial class Inherited : BaseClass
partial class Inherited
{
- [global::System.Runtime.CompilerServices.CompilerGenerated]
[global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCode]
- [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public Inherited(
global::IAnotherService anotherService,
global::IService service
@@ -173,7 +176,7 @@ partial class Inherited
}
}
```
-snippet source | anchor
+snippet source | anchor
@@ -212,10 +215,7 @@ public string RedirectedProperty => InitializerProperty;
partial class Properties
{
- [global::System.Runtime.CompilerServices.CompilerGenerated]
[global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCode]
- [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public Properties(
string getProperty,
string protectedProperty,
@@ -230,7 +230,7 @@ partial class Properties
}
}
```
-snippet source | anchor
+snippet source | anchor
@@ -271,10 +271,7 @@ public partial class PostConstruct
partial class PostConstruct
{
- [global::System.Runtime.CompilerServices.CompilerGenerated]
[global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCode]
- [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public PostConstruct(global::IService service)
{
this._service = service;
@@ -282,7 +279,7 @@ partial class PostConstruct
}
}
```
-snippet source | anchor
+snippet source | anchor
@@ -322,10 +319,7 @@ public partial class PostConstructWithParameter
partial class PostConstructWithParameter
{
- [global::System.Runtime.CompilerServices.CompilerGenerated]
[global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCode]
- [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public PostConstructWithParameter(
global::IService service,
global::IInitializeService initialiseService
@@ -336,7 +330,7 @@ partial class PostConstructWithParameter
}
}
```
-snippet source | anchor
+snippet source | anchor
@@ -378,10 +372,7 @@ public partial class PostConstructWithOutParameter
partial class PostConstructWithOutParameter
{
- [global::System.Runtime.CompilerServices.CompilerGenerated]
[global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCode]
- [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public PostConstructWithOutParameter(
global::IOtherService otherService,
global::IServiceProvider services
@@ -392,7 +383,7 @@ partial class PostConstructWithOutParameter
}
}
```
-snippet source | anchor
+snippet source | anchor
@@ -428,17 +419,14 @@ public partial class PostConstructWithDefaultParameter
partial class PostConstructWithDefaultParameter
{
- [global::System.Runtime.CompilerServices.CompilerGenerated]
[global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCode]
- [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public PostConstructWithDefaultParameter(global::Service service = default)
{
Initialize(service);
}
}
```
-snippet source | anchor
+snippet source | anchor
@@ -490,17 +478,14 @@ public partial class Guarded
partial class Guarded
{
- [global::System.Runtime.CompilerServices.CompilerGenerated]
[global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCode]
- [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public Guarded(global::IService service)
{
this._service = service ?? throw new global::System.ArgumentNullException("service");
}
}
```
-snippet source | anchor
+snippet source | anchor
@@ -537,10 +522,7 @@ public partial class Keyed
partial class Keyed
{
- [global::System.Runtime.CompilerServices.CompilerGenerated]
[global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCode]
- [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public Keyed(
[global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("key")] global::IService keyedService
)
@@ -549,11 +531,1961 @@ partial class Keyed
}
}
```
-snippet source | anchor
+snippet source | anchor
+Back to Contents
+## Service Provider
+
+
+
+```cs
+[ServiceProvider]
+[Singleton]
+[Singleton]
+public sealed partial class Container;
+
+[AutoConstruct]
+public partial class Greeter : IGreeter
+{
+ private readonly IClock _clock;
+}
+```
+snippet source | anchor
+
+
+What gets generated
+
+
+
+```cs
+//HintName: Container.ServiceProvider.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by https://github.com/distantcam/AutoCtor
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+partial class Container :
+ global::System.IServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService,
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService,
+ global::System.IDisposable,
+ Container.IResolver,
+ Container.IResolver,
+ Container.IResolver>,
+ Container.IResolver>,
+ Container.IResolver,
+ Container.IResolver,
+ Container.IResolver,
+ Container.IResolver,
+ Container.IResolver
+
+{
+ private readonly object _lock = new object();
+ private global::System.Collections.Generic.List
+
+Add `[ServiceProvider]` to a blank partial class and register services with `[Singleton]`, `[Transient]` or `[Scoped]` attributes.
+
+The service provider is fully generated at compile time so there is no runtime reflection.
+
+Multiple registrations for the same service can be returned with `IEnumerable`. When getting just one the last registration wins.
+
+
+### Scanning
+
+As well as registering services individually there is also an option to scan for services. Scanning is performed at compile time.
+
+
+
+```cs
+[ServiceProvider]
+[ScanTransient(typeof(IHandler<>))]
+public sealed partial class OrderContainer;
+
+public interface IHandler;
+
+public class CreateOrderHandler : IHandler;
+public class CancelOrderHandler : IHandler;
+```
+snippet source | anchor
+
+
+What gets generated
+
+
+
+```cs
+//HintName: OrderContainer.ServiceProvider.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by https://github.com/distantcam/AutoCtor
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+partial class OrderContainer :
+ global::System.IServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService,
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService,
+ global::System.IDisposable,
+ OrderContainer.IResolver>,
+ OrderContainer.IResolver>,
+ OrderContainer.IResolver>>,
+ OrderContainer.IResolver>>,
+ OrderContainer.IResolver,
+ OrderContainer.IResolver,
+ OrderContainer.IResolver,
+ OrderContainer.IResolver,
+ OrderContainer.IResolver
+
+{
+ private readonly object _lock = new object();
+ private global::System.Collections.Generic.List? _disposables;
+
+ private global::IHandler S0() => new global::CancelOrderHandler();
+ private global::IHandler S1() => new global::CreateOrderHandler();
+
+ private object? Resolve(global::System.Type type, object? key, bool probe)
+ {
+ if (key is null)
+ {
+ if (type == typeof(global::IHandler))
+ return probe ? this : (object)(S0());
+ if (type == typeof(global::IHandler))
+ return probe ? this : (object)(S1());
+ if (type == typeof(global::System.Collections.Generic.IEnumerable>))
+ return probe ? this : (object)(new global::IHandler[] { S0() });
+ if (type == typeof(global::System.Collections.Generic.IEnumerable>))
+ return probe ? this : (object)(new global::IHandler[] { S1() });
+ if (type == typeof(global::System.IServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService))
+ return probe ? this : (object)(this);
+ return null;
+ }
+ return null;
+ }
+
+ private global::System.IServiceProvider? Fallback => null;
+
+ public object? GetService(global::System.Type serviceType)
+ => GetKeyedService(serviceType, null);
+
+ public object? GetKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, false)
+ ?? (serviceKey == null ? Fallback?.GetService(serviceType) : (Fallback as global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider)?.GetKeyedService(serviceType, serviceKey));
+
+ public object GetRequiredKeyedService(global::System.Type serviceType, object? serviceKey)
+ => GetKeyedService(serviceType, serviceKey)
+ ?? throw new global::System.InvalidOperationException("No service for type '" + serviceType + "' has been registered.");
+
+ public T? GetService()
+ => this is OrderContainer.IResolver resolver ? resolver.Get() : GetService(typeof(T)) is T service ? service : default;
+
+ public T GetRequiredService()
+ => this is OrderContainer.IResolver resolver ? resolver.Get() : (T)GetRequiredKeyedService(typeof(T), null);
+
+ private interface IResolver { T Get(); }
+ global::IHandler OrderContainer.IResolver>.Get()
+ => S0();
+ global::IHandler OrderContainer.IResolver>.Get()
+ => S1();
+ global::System.Collections.Generic.IEnumerable> OrderContainer.IResolver>>.Get()
+ => new global::IHandler[] { S0() };
+ global::System.Collections.Generic.IEnumerable> OrderContainer.IResolver>>.Get()
+ => new global::IHandler[] { S1() };
+ global::System.IServiceProvider OrderContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory OrderContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService OrderContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider OrderContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService OrderContainer.IResolver.Get()
+ => this;
+
+ public bool IsService(global::System.Type serviceType)
+ => IsKeyedService(serviceType, null);
+
+ public bool IsKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, true) != null || (serviceKey == null ? Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService s && s.IsService(serviceType) : Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService k && k.IsKeyedService(serviceType, serviceKey));
+
+ public Scope CreateScope() => new Scope(this);
+ global::Microsoft.Extensions.DependencyInjection.IServiceScope global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory.CreateScope() => CreateScope();
+
+ private static T Required(object? service)
+ => service is T t ? t : throw new global::System.InvalidOperationException("No service for type '" + typeof(T) + "' has been registered.");
+
+ private T Track(T service)
+ {
+ if (service is global::System.IDisposable)
+ lock (_lock)
+ (_disposables ??= new global::System.Collections.Generic.List()).Add(service);
+ return service;
+ }
+
+ private object[] Drain()
+ {
+ lock (_lock)
+ {
+ var items = _disposables?.ToArray() ?? new object[0];
+ _disposables = null;
+ global::System.Array.Reverse(items);
+ return items;
+ }
+ }
+
+ public void Dispose()
+ {
+ foreach (var item in Drain())
+ {
+ if (item is global::System.IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ public sealed class Scope :
+ global::System.IServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService,
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService,
+ global::System.IDisposable,
+ global::Microsoft.Extensions.DependencyInjection.IServiceScope,
+ OrderContainer.IResolver>,
+ OrderContainer.IResolver>,
+ OrderContainer.IResolver>>,
+ OrderContainer.IResolver>>,
+ OrderContainer.IResolver,
+ OrderContainer.IResolver,
+ OrderContainer.IResolver,
+ OrderContainer.IResolver,
+ OrderContainer.IResolver
+
+ {
+ private readonly object _lock = new object();
+ private global::System.Collections.Generic.List? _disposables;
+ private readonly OrderContainer _root;
+ public Scope(OrderContainer root) => _root = root;
+ public global::System.IServiceProvider ServiceProvider => this;
+
+ private global::IHandler S0() => new global::CancelOrderHandler();
+ private global::IHandler S1() => new global::CreateOrderHandler();
+
+ private object? Resolve(global::System.Type type, object? key, bool probe)
+ {
+ if (key is null)
+ {
+ if (type == typeof(global::IHandler))
+ return probe ? this : (object)(S0());
+ if (type == typeof(global::IHandler))
+ return probe ? this : (object)(S1());
+ if (type == typeof(global::System.Collections.Generic.IEnumerable>))
+ return probe ? this : (object)(new global::IHandler[] { S0() });
+ if (type == typeof(global::System.Collections.Generic.IEnumerable>))
+ return probe ? this : (object)(new global::IHandler[] { S1() });
+ if (type == typeof(global::System.IServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService))
+ return probe ? this : (object)(this);
+ return null;
+ }
+ return null;
+ }
+
+ private global::System.IServiceProvider? Fallback => null;
+
+ public object? GetService(global::System.Type serviceType)
+ => GetKeyedService(serviceType, null);
+
+ public object? GetKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, false)
+ ?? (serviceKey == null ? Fallback?.GetService(serviceType) : (Fallback as global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider)?.GetKeyedService(serviceType, serviceKey));
+
+ public object GetRequiredKeyedService(global::System.Type serviceType, object? serviceKey)
+ => GetKeyedService(serviceType, serviceKey)
+ ?? throw new global::System.InvalidOperationException("No service for type '" + serviceType + "' has been registered.");
+
+ public T? GetService()
+ => this is OrderContainer.IResolver resolver ? resolver.Get() : GetService(typeof(T)) is T service ? service : default;
+
+ public T GetRequiredService()
+ => this is OrderContainer.IResolver resolver ? resolver.Get() : (T)GetRequiredKeyedService(typeof(T), null);
+
+ global::IHandler OrderContainer.IResolver>.Get()
+ => S0();
+ global::IHandler OrderContainer.IResolver>.Get()
+ => S1();
+ global::System.Collections.Generic.IEnumerable> OrderContainer.IResolver>>.Get()
+ => new global::IHandler[] { S0() };
+ global::System.Collections.Generic.IEnumerable> OrderContainer.IResolver>>.Get()
+ => new global::IHandler[] { S1() };
+ global::System.IServiceProvider OrderContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory OrderContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService OrderContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider OrderContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService OrderContainer.IResolver.Get()
+ => this;
+
+ public bool IsService(global::System.Type serviceType)
+ => IsKeyedService(serviceType, null);
+
+ public bool IsKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, true) != null || (serviceKey == null ? Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService s && s.IsService(serviceType) : Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService k && k.IsKeyedService(serviceType, serviceKey));
+
+ public Scope CreateScope() => new Scope(_root);
+ global::Microsoft.Extensions.DependencyInjection.IServiceScope global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory.CreateScope() => CreateScope();
+
+ private static T Required(object? service)
+ => service is T t ? t : throw new global::System.InvalidOperationException("No service for type '" + typeof(T) + "' has been registered.");
+
+ private T Track(T service)
+ {
+ if (service is global::System.IDisposable)
+ lock (_lock)
+ (_disposables ??= new global::System.Collections.Generic.List()).Add(service);
+ return service;
+ }
+
+ private object[] Drain()
+ {
+ lock (_lock)
+ {
+ var items = _disposables?.ToArray() ?? new object[0];
+ _disposables = null;
+ global::System.Array.Reverse(items);
+ return items;
+ }
+ }
+
+ public void Dispose()
+ {
+ foreach (var item in Drain())
+ {
+ if (item is global::System.IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+ }
+}
+```
+snippet source | anchor
+
+
+
+
+The scan attributes `[ScanSingleton]`, `[ScanTransient]`, and `[ScanScoped]` have some options.
+
+- `As` determines what to register the service as: `ScanAs.Service`, the service being scanned for (default); `ScanAs.Self`, the type itself; `ScanAs.ImplementedInterfaces`, all the interfaces the type has.
+- `FromAssembliesOf` can be used to scan other assemblies instead of the one the provider is in; add the provider too to keep it in the list of assemblies to scan.
+- `TypeNameFilter` only includes types whose name matches, where `*` matches anything and `?` any one character, e.g. `"*Repository"`. The service can be left out to select by name alone.
+
+### Keyed Services
+
+
+
+```cs
+[ServiceProvider]
+[Singleton(Key = "live")]
+[Singleton(Key = "sandbox")]
+[Singleton]
+public sealed partial class PaymentContainer;
+
+public interface IPaymentGateway;
+
+public class PaymentGateway : IPaymentGateway
+{
+ public PaymentGateway([AutoKeyedService("live")] IClient client) { }
+}
+```
+snippet source | anchor
+
+
+What gets generated
+
+
+
+```cs
+//HintName: PaymentContainer.ServiceProvider.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by https://github.com/distantcam/AutoCtor
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+partial class PaymentContainer :
+ global::System.IServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService,
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService,
+ global::System.IDisposable,
+ PaymentContainer.IResolver,
+ PaymentContainer.IResolver>,
+ PaymentContainer.IResolver,
+ PaymentContainer.IResolver,
+ PaymentContainer.IResolver,
+ PaymentContainer.IResolver,
+ PaymentContainer.IResolver
+
+{
+ private readonly object _lock = new object();
+ private global::System.Collections.Generic.List? _disposables;
+
+ private volatile global::IClient? _S0;
+ private global::IClient S0()
+ {
+ var service = _S0;
+ if (service is not null)
+ return service;
+ lock (_lock)
+ return (_S0 ??= new global::LiveClient());
+ }
+ private volatile global::IClient? _S1;
+ private global::IClient S1()
+ {
+ var service = _S1;
+ if (service is not null)
+ return service;
+ lock (_lock)
+ return (_S1 ??= new global::SandboxClient());
+ }
+ private volatile global::IPaymentGateway? _S2;
+ private global::IPaymentGateway S2()
+ {
+ var service = _S2;
+ if (service is not null)
+ return service;
+ lock (_lock)
+ return (_S2 ??= new global::PaymentGateway(S0()));
+ }
+
+ private object? Resolve(global::System.Type type, object? key, bool probe)
+ {
+ if (key is null)
+ {
+ if (type == typeof(global::IPaymentGateway))
+ return probe ? this : (object)(S2());
+ if (type == typeof(global::System.Collections.Generic.IEnumerable))
+ return probe ? this : (object)(new global::IPaymentGateway[] { S2() });
+ if (type == typeof(global::System.IServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService))
+ return probe ? this : (object)(this);
+ return null;
+ }
+ if (type == typeof(global::IClient) && object.Equals(key, "live"))
+ return probe ? this : (object)(S0());
+ if (type == typeof(global::IClient) && object.Equals(key, "sandbox"))
+ return probe ? this : (object)(S1());
+ if (type == typeof(global::System.Collections.Generic.IEnumerable) && object.Equals(key, "live"))
+ return probe ? this : (object)(new global::IClient[] { S0() });
+ if (type == typeof(global::System.Collections.Generic.IEnumerable) && object.Equals(key, "sandbox"))
+ return probe ? this : (object)(new global::IClient[] { S1() });
+ if (type == typeof(global::System.Collections.Generic.IEnumerable) && object.Equals(key, global::Microsoft.Extensions.DependencyInjection.KeyedService.AnyKey))
+ return probe ? this : (object)(new global::IClient[] { S0(), S1() });
+ return null;
+ }
+
+ private global::System.IServiceProvider? Fallback => null;
+
+ public object? GetService(global::System.Type serviceType)
+ => GetKeyedService(serviceType, null);
+
+ public object? GetKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, false)
+ ?? (serviceKey == null ? Fallback?.GetService(serviceType) : (Fallback as global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider)?.GetKeyedService(serviceType, serviceKey));
+
+ public object GetRequiredKeyedService(global::System.Type serviceType, object? serviceKey)
+ => GetKeyedService(serviceType, serviceKey)
+ ?? throw new global::System.InvalidOperationException("No service for type '" + serviceType + "' has been registered.");
+
+ public T? GetService()
+ => this is PaymentContainer.IResolver resolver ? resolver.Get() : GetService(typeof(T)) is T service ? service : default;
+
+ public T GetRequiredService()
+ => this is PaymentContainer.IResolver resolver ? resolver.Get() : (T)GetRequiredKeyedService(typeof(T), null);
+
+ private interface IResolver { T Get(); }
+ global::IPaymentGateway PaymentContainer.IResolver.Get()
+ => S2();
+ global::System.Collections.Generic.IEnumerable PaymentContainer.IResolver>.Get()
+ => new global::IPaymentGateway[] { S2() };
+ global::System.IServiceProvider PaymentContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory PaymentContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService PaymentContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider PaymentContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService PaymentContainer.IResolver.Get()
+ => this;
+
+ public bool IsService(global::System.Type serviceType)
+ => IsKeyedService(serviceType, null);
+
+ public bool IsKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, true) != null || (serviceKey == null ? Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService s && s.IsService(serviceType) : Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService k && k.IsKeyedService(serviceType, serviceKey));
+
+ public Scope CreateScope() => new Scope(this);
+ global::Microsoft.Extensions.DependencyInjection.IServiceScope global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory.CreateScope() => CreateScope();
+
+ private static T Required(object? service)
+ => service is T t ? t : throw new global::System.InvalidOperationException("No service for type '" + typeof(T) + "' has been registered.");
+
+ private T Track(T service)
+ {
+ if (service is global::System.IDisposable)
+ lock (_lock)
+ (_disposables ??= new global::System.Collections.Generic.List()).Add(service);
+ return service;
+ }
+
+ private object[] Drain()
+ {
+ lock (_lock)
+ {
+ var items = _disposables?.ToArray() ?? new object[0];
+ _disposables = null;
+ global::System.Array.Reverse(items);
+ return items;
+ }
+ }
+
+ public void Dispose()
+ {
+ foreach (var item in Drain())
+ {
+ if (item is global::System.IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ public sealed class Scope :
+ global::System.IServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService,
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService,
+ global::System.IDisposable,
+ global::Microsoft.Extensions.DependencyInjection.IServiceScope,
+ PaymentContainer.IResolver,
+ PaymentContainer.IResolver>,
+ PaymentContainer.IResolver,
+ PaymentContainer.IResolver,
+ PaymentContainer.IResolver,
+ PaymentContainer.IResolver,
+ PaymentContainer.IResolver
+
+ {
+ private readonly object _lock = new object();
+ private global::System.Collections.Generic.List? _disposables;
+ private readonly PaymentContainer _root;
+ public Scope(PaymentContainer root) => _root = root;
+ public global::System.IServiceProvider ServiceProvider => this;
+
+ private global::IClient S0() => _root.S0();
+ private global::IClient S1() => _root.S1();
+ private global::IPaymentGateway S2() => _root.S2();
+
+ private object? Resolve(global::System.Type type, object? key, bool probe)
+ {
+ if (key is null)
+ {
+ if (type == typeof(global::IPaymentGateway))
+ return probe ? this : (object)(S2());
+ if (type == typeof(global::System.Collections.Generic.IEnumerable))
+ return probe ? this : (object)(new global::IPaymentGateway[] { S2() });
+ if (type == typeof(global::System.IServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService))
+ return probe ? this : (object)(this);
+ return null;
+ }
+ if (type == typeof(global::IClient) && object.Equals(key, "live"))
+ return probe ? this : (object)(S0());
+ if (type == typeof(global::IClient) && object.Equals(key, "sandbox"))
+ return probe ? this : (object)(S1());
+ if (type == typeof(global::System.Collections.Generic.IEnumerable) && object.Equals(key, "live"))
+ return probe ? this : (object)(new global::IClient[] { S0() });
+ if (type == typeof(global::System.Collections.Generic.IEnumerable) && object.Equals(key, "sandbox"))
+ return probe ? this : (object)(new global::IClient[] { S1() });
+ if (type == typeof(global::System.Collections.Generic.IEnumerable) && object.Equals(key, global::Microsoft.Extensions.DependencyInjection.KeyedService.AnyKey))
+ return probe ? this : (object)(new global::IClient[] { S0(), S1() });
+ return null;
+ }
+
+ private global::System.IServiceProvider? Fallback => null;
+
+ public object? GetService(global::System.Type serviceType)
+ => GetKeyedService(serviceType, null);
+
+ public object? GetKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, false)
+ ?? (serviceKey == null ? Fallback?.GetService(serviceType) : (Fallback as global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider)?.GetKeyedService(serviceType, serviceKey));
+
+ public object GetRequiredKeyedService(global::System.Type serviceType, object? serviceKey)
+ => GetKeyedService(serviceType, serviceKey)
+ ?? throw new global::System.InvalidOperationException("No service for type '" + serviceType + "' has been registered.");
+
+ public T? GetService()
+ => this is PaymentContainer.IResolver resolver ? resolver.Get() : GetService(typeof(T)) is T service ? service : default;
+
+ public T GetRequiredService()
+ => this is PaymentContainer.IResolver resolver ? resolver.Get() : (T)GetRequiredKeyedService(typeof(T), null);
+
+ global::IPaymentGateway PaymentContainer.IResolver.Get()
+ => S2();
+ global::System.Collections.Generic.IEnumerable PaymentContainer.IResolver>.Get()
+ => new global::IPaymentGateway[] { S2() };
+ global::System.IServiceProvider PaymentContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory PaymentContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService PaymentContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider PaymentContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService PaymentContainer.IResolver.Get()
+ => this;
+
+ public bool IsService(global::System.Type serviceType)
+ => IsKeyedService(serviceType, null);
+
+ public bool IsKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, true) != null || (serviceKey == null ? Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService s && s.IsService(serviceType) : Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService k && k.IsKeyedService(serviceType, serviceKey));
+
+ public Scope CreateScope() => new Scope(_root);
+ global::Microsoft.Extensions.DependencyInjection.IServiceScope global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory.CreateScope() => CreateScope();
+
+ private static T Required(object? service)
+ => service is T t ? t : throw new global::System.InvalidOperationException("No service for type '" + typeof(T) + "' has been registered.");
+
+ private T Track(T service)
+ {
+ if (service is global::System.IDisposable)
+ lock (_lock)
+ (_disposables ??= new global::System.Collections.Generic.List()).Add(service);
+ return service;
+ }
+
+ private object[] Drain()
+ {
+ lock (_lock)
+ {
+ var items = _disposables?.ToArray() ?? new object[0];
+ _disposables = null;
+ global::System.Array.Reverse(items);
+ return items;
+ }
+ }
+
+ public void Dispose()
+ {
+ foreach (var item in Drain())
+ {
+ if (item is global::System.IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+ }
+}
+```
+snippet source | anchor
+
+
+
+
+### Factories
+
+
+
+```cs
+[ServiceProvider]
+[Singleton(Factory = nameof(CreateClock))]
+[Singleton(Factory = nameof(_banner))]
+[Scoped(Factory = nameof(CreateAuditLog))]
+public sealed partial class HostContainer
+{
+ private readonly IBanner _banner = new Banner("AutoCtor");
+
+ private IClock CreateClock() => new SystemClock("UTC");
+
+ private IAuditLog CreateAuditLog(System.IServiceProvider services) => new AuditLog(services);
+}
+```
+snippet source | anchor
+
+
+What gets generated
+
+
+
+```cs
+//HintName: HostContainer.ServiceProvider.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by https://github.com/distantcam/AutoCtor
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+partial class HostContainer :
+ global::System.IServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService,
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService,
+ global::System.IDisposable,
+ HostContainer.IResolver,
+ HostContainer.IResolver,
+ HostContainer.IResolver>,
+ HostContainer.IResolver>,
+ HostContainer.IResolver,
+ HostContainer.IResolver,
+ HostContainer.IResolver,
+ HostContainer.IResolver,
+ HostContainer.IResolver
+
+{
+ private readonly object _lock = new object();
+ private global::System.Collections.Generic.List? _disposables;
+
+ private volatile global::IClock? _S0;
+ private global::IClock S0()
+ {
+ var service = _S0;
+ if (service is not null)
+ return service;
+ lock (_lock)
+ return (_S0 ??= Track(this.CreateClock()));
+ }
+ private volatile global::IBanner? _S1;
+ private global::IBanner S1()
+ {
+ var service = _S1;
+ if (service is not null)
+ return service;
+ lock (_lock)
+ return (_S1 ??= Track(this._banner));
+ }
+
+ private object? Resolve(global::System.Type type, object? key, bool probe)
+ {
+ if (key is null)
+ {
+ if (type == typeof(global::IClock))
+ return probe ? this : (object)(S0());
+ if (type == typeof(global::IBanner))
+ return probe ? this : (object)(S1());
+ if (type == typeof(global::System.Collections.Generic.IEnumerable))
+ return probe ? this : (object)(new global::IClock[] { S0() });
+ if (type == typeof(global::System.Collections.Generic.IEnumerable))
+ return probe ? this : (object)(new global::IBanner[] { S1() });
+ if (type == typeof(global::System.IServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService))
+ return probe ? this : (object)(this);
+ return null;
+ }
+ return null;
+ }
+
+ private global::System.IServiceProvider? Fallback => null;
+
+ public object? GetService(global::System.Type serviceType)
+ => GetKeyedService(serviceType, null);
+
+ public object? GetKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, false)
+ ?? (serviceKey == null ? Fallback?.GetService(serviceType) : (Fallback as global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider)?.GetKeyedService(serviceType, serviceKey));
+
+ public object GetRequiredKeyedService(global::System.Type serviceType, object? serviceKey)
+ => GetKeyedService(serviceType, serviceKey)
+ ?? throw new global::System.InvalidOperationException("No service for type '" + serviceType + "' has been registered.");
+
+ public T? GetService()
+ => this is HostContainer.IResolver resolver ? resolver.Get() : GetService(typeof(T)) is T service ? service : default;
+
+ public T GetRequiredService()
+ => this is HostContainer.IResolver resolver ? resolver.Get() : (T)GetRequiredKeyedService(typeof(T), null);
+
+ private interface IResolver { T Get(); }
+ global::IClock HostContainer.IResolver.Get()
+ => S0();
+ global::IBanner HostContainer.IResolver.Get()
+ => S1();
+ global::System.Collections.Generic.IEnumerable HostContainer.IResolver>.Get()
+ => new global::IClock[] { S0() };
+ global::System.Collections.Generic.IEnumerable HostContainer.IResolver>.Get()
+ => new global::IBanner[] { S1() };
+ global::System.IServiceProvider HostContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory HostContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService HostContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider HostContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService HostContainer.IResolver.Get()
+ => this;
+
+ public bool IsService(global::System.Type serviceType)
+ => IsKeyedService(serviceType, null);
+
+ public bool IsKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, true) != null || (serviceKey == null ? Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService s && s.IsService(serviceType) : Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService k && k.IsKeyedService(serviceType, serviceKey));
+
+ public Scope CreateScope() => new Scope(this);
+ global::Microsoft.Extensions.DependencyInjection.IServiceScope global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory.CreateScope() => CreateScope();
+
+ private static T Required(object? service)
+ => service is T t ? t : throw new global::System.InvalidOperationException("No service for type '" + typeof(T) + "' has been registered.");
+
+ private T Track(T service)
+ {
+ if (service is global::System.IDisposable)
+ lock (_lock)
+ (_disposables ??= new global::System.Collections.Generic.List()).Add(service);
+ return service;
+ }
+
+ private object[] Drain()
+ {
+ lock (_lock)
+ {
+ var items = _disposables?.ToArray() ?? new object[0];
+ _disposables = null;
+ global::System.Array.Reverse(items);
+ return items;
+ }
+ }
+
+ public void Dispose()
+ {
+ foreach (var item in Drain())
+ {
+ if (item is global::System.IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ public sealed class Scope :
+ global::System.IServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService,
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService,
+ global::System.IDisposable,
+ global::Microsoft.Extensions.DependencyInjection.IServiceScope,
+ HostContainer.IResolver,
+ HostContainer.IResolver,
+ HostContainer.IResolver,
+ HostContainer.IResolver>,
+ HostContainer.IResolver>,
+ HostContainer.IResolver>,
+ HostContainer.IResolver,
+ HostContainer.IResolver,
+ HostContainer.IResolver,
+ HostContainer.IResolver,
+ HostContainer.IResolver
+
+ {
+ private readonly object _lock = new object();
+ private global::System.Collections.Generic.List? _disposables;
+ private readonly HostContainer _root;
+ public Scope(HostContainer root) => _root = root;
+ public global::System.IServiceProvider ServiceProvider => this;
+
+ private global::IClock S0() => _root.S0();
+ private global::IBanner S1() => _root.S1();
+ private volatile global::IAuditLog? _S2;
+ private global::IAuditLog S2()
+ {
+ var service = _S2;
+ if (service is not null)
+ return service;
+ lock (_lock)
+ return (_S2 ??= Track(_root.CreateAuditLog(this)));
+ }
+
+ private object? Resolve(global::System.Type type, object? key, bool probe)
+ {
+ if (key is null)
+ {
+ if (type == typeof(global::IClock))
+ return probe ? this : (object)(S0());
+ if (type == typeof(global::IBanner))
+ return probe ? this : (object)(S1());
+ if (type == typeof(global::IAuditLog))
+ return probe ? this : (object)(S2());
+ if (type == typeof(global::System.Collections.Generic.IEnumerable))
+ return probe ? this : (object)(new global::IClock[] { S0() });
+ if (type == typeof(global::System.Collections.Generic.IEnumerable))
+ return probe ? this : (object)(new global::IBanner[] { S1() });
+ if (type == typeof(global::System.Collections.Generic.IEnumerable))
+ return probe ? this : (object)(new global::IAuditLog[] { S2() });
+ if (type == typeof(global::System.IServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService))
+ return probe ? this : (object)(this);
+ return null;
+ }
+ return null;
+ }
+
+ private global::System.IServiceProvider? Fallback => null;
+
+ public object? GetService(global::System.Type serviceType)
+ => GetKeyedService(serviceType, null);
+
+ public object? GetKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, false)
+ ?? (serviceKey == null ? Fallback?.GetService(serviceType) : (Fallback as global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider)?.GetKeyedService(serviceType, serviceKey));
+
+ public object GetRequiredKeyedService(global::System.Type serviceType, object? serviceKey)
+ => GetKeyedService(serviceType, serviceKey)
+ ?? throw new global::System.InvalidOperationException("No service for type '" + serviceType + "' has been registered.");
+
+ public T? GetService()
+ => this is HostContainer.IResolver resolver ? resolver.Get() : GetService(typeof(T)) is T service ? service : default;
+
+ public T GetRequiredService()
+ => this is HostContainer.IResolver resolver ? resolver.Get() : (T)GetRequiredKeyedService(typeof(T), null);
+
+ global::IClock HostContainer.IResolver.Get()
+ => S0();
+ global::IBanner HostContainer.IResolver.Get()
+ => S1();
+ global::IAuditLog HostContainer.IResolver.Get()
+ => S2();
+ global::System.Collections.Generic.IEnumerable HostContainer.IResolver>.Get()
+ => new global::IClock[] { S0() };
+ global::System.Collections.Generic.IEnumerable HostContainer.IResolver>.Get()
+ => new global::IBanner[] { S1() };
+ global::System.Collections.Generic.IEnumerable HostContainer.IResolver>.Get()
+ => new global::IAuditLog[] { S2() };
+ global::System.IServiceProvider HostContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory HostContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService HostContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider HostContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService HostContainer.IResolver.Get()
+ => this;
+
+ public bool IsService(global::System.Type serviceType)
+ => IsKeyedService(serviceType, null);
+
+ public bool IsKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, true) != null || (serviceKey == null ? Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService s && s.IsService(serviceType) : Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService k && k.IsKeyedService(serviceType, serviceKey));
+
+ public Scope CreateScope() => new Scope(_root);
+ global::Microsoft.Extensions.DependencyInjection.IServiceScope global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory.CreateScope() => CreateScope();
+
+ private static T Required(object? service)
+ => service is T t ? t : throw new global::System.InvalidOperationException("No service for type '" + typeof(T) + "' has been registered.");
+
+ private T Track(T service)
+ {
+ if (service is global::System.IDisposable)
+ lock (_lock)
+ (_disposables ??= new global::System.Collections.Generic.List()).Add(service);
+ return service;
+ }
+
+ private object[] Drain()
+ {
+ lock (_lock)
+ {
+ var items = _disposables?.ToArray() ?? new object[0];
+ _disposables = null;
+ global::System.Array.Reverse(items);
+ return items;
+ }
+ }
+
+ public void Dispose()
+ {
+ foreach (var item in Drain())
+ {
+ if (item is global::System.IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+ }
+}
+```
+snippet source | anchor
+
+
+
+
+### Falling back to another provider
+
+`Fallback` on `[ServiceProvider]` names a member to use when there is no registration for that service.
+
+
+
+```cs
+[ServiceProvider(Fallback = nameof(_host))]
+[Singleton]
+public sealed partial class FallbackContainer
+{
+ private readonly System.IServiceProvider _host;
+
+ public FallbackContainer(System.IServiceProvider host) => _host = host;
+}
+```
+snippet source | anchor
+
+
+What gets generated
+
+
+
+```cs
+//HintName: FallbackContainer.ServiceProvider.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by https://github.com/distantcam/AutoCtor
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+partial class FallbackContainer :
+ global::System.IServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService,
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService,
+ global::System.IDisposable,
+ FallbackContainer.IResolver,
+ FallbackContainer.IResolver>,
+ FallbackContainer.IResolver,
+ FallbackContainer.IResolver,
+ FallbackContainer.IResolver,
+ FallbackContainer.IResolver,
+ FallbackContainer.IResolver
+
+{
+ private readonly object _lock = new object();
+ private global::System.Collections.Generic.List? _disposables;
+
+ private volatile global::IGreeter? _S0;
+ private global::IGreeter S0()
+ {
+ var service = _S0;
+ if (service is not null)
+ return service;
+ lock (_lock)
+ return (_S0 ??= new global::Greeter(Required(GetKeyedService(typeof(global::ILogSink), null))));
+ }
+
+ private object? Resolve(global::System.Type type, object? key, bool probe)
+ {
+ if (key is null)
+ {
+ if (type == typeof(global::IGreeter))
+ return probe ? this : (object)(S0());
+ if (type == typeof(global::System.Collections.Generic.IEnumerable))
+ return probe ? this : (object)(new global::IGreeter[] { S0() });
+ if (type == typeof(global::System.IServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService))
+ return probe ? this : (object)(this);
+ return null;
+ }
+ return null;
+ }
+
+ private global::System.IServiceProvider? Fallback => this._host;
+
+ public object? GetService(global::System.Type serviceType)
+ => GetKeyedService(serviceType, null);
+
+ public object? GetKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, false)
+ ?? (serviceKey == null ? Fallback?.GetService(serviceType) : (Fallback as global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider)?.GetKeyedService(serviceType, serviceKey));
+
+ public object GetRequiredKeyedService(global::System.Type serviceType, object? serviceKey)
+ => GetKeyedService(serviceType, serviceKey)
+ ?? throw new global::System.InvalidOperationException("No service for type '" + serviceType + "' has been registered.");
+
+ public T? GetService()
+ => this is FallbackContainer.IResolver resolver ? resolver.Get() : GetService(typeof(T)) is T service ? service : default;
+
+ public T GetRequiredService()
+ => this is FallbackContainer.IResolver resolver ? resolver.Get() : (T)GetRequiredKeyedService(typeof(T), null);
+
+ private interface IResolver { T Get(); }
+ global::IGreeter FallbackContainer.IResolver.Get()
+ => S0();
+ global::System.Collections.Generic.IEnumerable FallbackContainer.IResolver>.Get()
+ => new global::IGreeter[] { S0() };
+ global::System.IServiceProvider FallbackContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory FallbackContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService FallbackContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider FallbackContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService FallbackContainer.IResolver.Get()
+ => this;
+
+ public bool IsService(global::System.Type serviceType)
+ => IsKeyedService(serviceType, null);
+
+ public bool IsKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, true) != null || (serviceKey == null ? Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService s && s.IsService(serviceType) : Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService k && k.IsKeyedService(serviceType, serviceKey));
+
+ public Scope CreateScope() => new Scope(this);
+ global::Microsoft.Extensions.DependencyInjection.IServiceScope global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory.CreateScope() => CreateScope();
+
+ private static T Required(object? service)
+ => service is T t ? t : throw new global::System.InvalidOperationException("No service for type '" + typeof(T) + "' has been registered.");
+
+ private T Track(T service)
+ {
+ if (service is global::System.IDisposable)
+ lock (_lock)
+ (_disposables ??= new global::System.Collections.Generic.List()).Add(service);
+ return service;
+ }
+
+ private object[] Drain()
+ {
+ lock (_lock)
+ {
+ var items = _disposables?.ToArray() ?? new object[0];
+ _disposables = null;
+ global::System.Array.Reverse(items);
+ return items;
+ }
+ }
+
+ public void Dispose()
+ {
+ foreach (var item in Drain())
+ {
+ if (item is global::System.IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ public sealed class Scope :
+ global::System.IServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService,
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService,
+ global::System.IDisposable,
+ global::Microsoft.Extensions.DependencyInjection.IServiceScope,
+ FallbackContainer.IResolver,
+ FallbackContainer.IResolver>,
+ FallbackContainer.IResolver,
+ FallbackContainer.IResolver,
+ FallbackContainer.IResolver,
+ FallbackContainer.IResolver,
+ FallbackContainer.IResolver
+
+ {
+ private readonly object _lock = new object();
+ private global::System.Collections.Generic.List? _disposables;
+ private readonly FallbackContainer _root;
+ public Scope(FallbackContainer root) => _root = root;
+ public global::System.IServiceProvider ServiceProvider => this;
+
+ private global::IGreeter S0() => _root.S0();
+
+ private object? Resolve(global::System.Type type, object? key, bool probe)
+ {
+ if (key is null)
+ {
+ if (type == typeof(global::IGreeter))
+ return probe ? this : (object)(S0());
+ if (type == typeof(global::System.Collections.Generic.IEnumerable))
+ return probe ? this : (object)(new global::IGreeter[] { S0() });
+ if (type == typeof(global::System.IServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService))
+ return probe ? this : (object)(this);
+ return null;
+ }
+ return null;
+ }
+
+ private global::System.IServiceProvider? _fallback;
+ private global::System.IServiceProvider? Fallback { get { lock (_lock) return _fallback ??= _root._host is global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory f ? Track(f.CreateScope()).ServiceProvider : _root._host; } }
+
+ public object? GetService(global::System.Type serviceType)
+ => GetKeyedService(serviceType, null);
+
+ public object? GetKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, false)
+ ?? (serviceKey == null ? Fallback?.GetService(serviceType) : (Fallback as global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider)?.GetKeyedService(serviceType, serviceKey));
+
+ public object GetRequiredKeyedService(global::System.Type serviceType, object? serviceKey)
+ => GetKeyedService(serviceType, serviceKey)
+ ?? throw new global::System.InvalidOperationException("No service for type '" + serviceType + "' has been registered.");
+
+ public T? GetService()
+ => this is FallbackContainer.IResolver resolver ? resolver.Get() : GetService(typeof(T)) is T service ? service : default;
+
+ public T GetRequiredService()
+ => this is FallbackContainer.IResolver resolver ? resolver.Get() : (T)GetRequiredKeyedService(typeof(T), null);
+
+ global::IGreeter FallbackContainer.IResolver.Get()
+ => S0();
+ global::System.Collections.Generic.IEnumerable FallbackContainer.IResolver>.Get()
+ => new global::IGreeter[] { S0() };
+ global::System.IServiceProvider FallbackContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory FallbackContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService FallbackContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider FallbackContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService FallbackContainer.IResolver.Get()
+ => this;
+
+ public bool IsService(global::System.Type serviceType)
+ => IsKeyedService(serviceType, null);
+
+ public bool IsKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, true) != null || (serviceKey == null ? Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService s && s.IsService(serviceType) : Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService k && k.IsKeyedService(serviceType, serviceKey));
+
+ public Scope CreateScope() => new Scope(_root);
+ global::Microsoft.Extensions.DependencyInjection.IServiceScope global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory.CreateScope() => CreateScope();
+
+ private static T Required(object? service)
+ => service is T t ? t : throw new global::System.InvalidOperationException("No service for type '" + typeof(T) + "' has been registered.");
+
+ private T Track(T service)
+ {
+ if (service is global::System.IDisposable)
+ lock (_lock)
+ (_disposables ??= new global::System.Collections.Generic.List()).Add(service);
+ return service;
+ }
+
+ private object[] Drain()
+ {
+ lock (_lock)
+ {
+ var items = _disposables?.ToArray() ?? new object[0];
+ _disposables = null;
+ global::System.Array.Reverse(items);
+ return items;
+ }
+ }
+
+ public void Dispose()
+ {
+ foreach (var item in Drain())
+ {
+ if (item is global::System.IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+ }
+}
+```
+snippet source | anchor
+
+
+
+
+### Modules
+
+`[Import]` (or `[Import(typeof(TModule))]`) adds all the registrations from another type. Modules just act as markers so no code is generated for them.
+
+
+
+```cs
+[Singleton]
+[Scoped(Factory = nameof(CreateLedger))]
+public class PaymentsModule
+{
+ public static ILedger CreateLedger(IPaymentGateway gateway) => new Ledger(gateway);
+}
+
+[ServiceProvider]
+[Import]
+[Singleton]
+public sealed partial class TestContainer;
+```
+snippet source | anchor
+
+
+What gets generated
+
+
+
+```cs
+//HintName: TestContainer.ServiceProvider.g.cs
+//------------------------------------------------------------------------------
+//
+// This code was generated by https://github.com/distantcam/AutoCtor
+//
+//------------------------------------------------------------------------------
+
+#nullable enable
+
+partial class TestContainer :
+ global::System.IServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService,
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService,
+ global::System.IDisposable,
+ TestContainer.IResolver,
+ TestContainer.IResolver>,
+ TestContainer.IResolver,
+ TestContainer.IResolver,
+ TestContainer.IResolver,
+ TestContainer.IResolver,
+ TestContainer.IResolver
+
+{
+ private readonly object _lock = new object();
+ private global::System.Collections.Generic.List? _disposables;
+
+ private volatile global::IPaymentGateway? _S0;
+ private global::IPaymentGateway S0()
+ {
+ var service = _S0;
+ if (service is not null)
+ return service;
+ lock (_lock)
+ return (_S0 ??= new global::StripeGateway());
+ }
+ private volatile global::IPaymentGateway? _S2;
+ private global::IPaymentGateway S2()
+ {
+ var service = _S2;
+ if (service is not null)
+ return service;
+ lock (_lock)
+ return (_S2 ??= new global::FakeGateway());
+ }
+
+ private object? Resolve(global::System.Type type, object? key, bool probe)
+ {
+ if (key is null)
+ {
+ if (type == typeof(global::IPaymentGateway))
+ return probe ? this : (object)(S2());
+ if (type == typeof(global::System.Collections.Generic.IEnumerable))
+ return probe ? this : (object)(new global::IPaymentGateway[] { S0(), S2() });
+ if (type == typeof(global::System.IServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService))
+ return probe ? this : (object)(this);
+ return null;
+ }
+ return null;
+ }
+
+ private global::System.IServiceProvider? Fallback => null;
+
+ public object? GetService(global::System.Type serviceType)
+ => GetKeyedService(serviceType, null);
+
+ public object? GetKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, false)
+ ?? (serviceKey == null ? Fallback?.GetService(serviceType) : (Fallback as global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider)?.GetKeyedService(serviceType, serviceKey));
+
+ public object GetRequiredKeyedService(global::System.Type serviceType, object? serviceKey)
+ => GetKeyedService(serviceType, serviceKey)
+ ?? throw new global::System.InvalidOperationException("No service for type '" + serviceType + "' has been registered.");
+
+ public T? GetService()
+ => this is TestContainer.IResolver resolver ? resolver.Get() : GetService(typeof(T)) is T service ? service : default;
+
+ public T GetRequiredService()
+ => this is TestContainer.IResolver resolver ? resolver.Get() : (T)GetRequiredKeyedService(typeof(T), null);
+
+ private interface IResolver { T Get(); }
+ global::IPaymentGateway TestContainer.IResolver.Get()
+ => S2();
+ global::System.Collections.Generic.IEnumerable TestContainer.IResolver>.Get()
+ => new global::IPaymentGateway[] { S0(), S2() };
+ global::System.IServiceProvider TestContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory TestContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService TestContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider TestContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService TestContainer.IResolver.Get()
+ => this;
+
+ public bool IsService(global::System.Type serviceType)
+ => IsKeyedService(serviceType, null);
+
+ public bool IsKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, true) != null || (serviceKey == null ? Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService s && s.IsService(serviceType) : Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService k && k.IsKeyedService(serviceType, serviceKey));
+
+ public Scope CreateScope() => new Scope(this);
+ global::Microsoft.Extensions.DependencyInjection.IServiceScope global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory.CreateScope() => CreateScope();
+
+ private static T Required(object? service)
+ => service is T t ? t : throw new global::System.InvalidOperationException("No service for type '" + typeof(T) + "' has been registered.");
+
+ private T Track(T service)
+ {
+ if (service is global::System.IDisposable)
+ lock (_lock)
+ (_disposables ??= new global::System.Collections.Generic.List()).Add(service);
+ return service;
+ }
+
+ private object[] Drain()
+ {
+ lock (_lock)
+ {
+ var items = _disposables?.ToArray() ?? new object[0];
+ _disposables = null;
+ global::System.Array.Reverse(items);
+ return items;
+ }
+ }
+
+ public void Dispose()
+ {
+ foreach (var item in Drain())
+ {
+ if (item is global::System.IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ public sealed class Scope :
+ global::System.IServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService,
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider,
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService,
+ global::System.IDisposable,
+ global::Microsoft.Extensions.DependencyInjection.IServiceScope,
+ TestContainer.IResolver,
+ TestContainer.IResolver,
+ TestContainer.IResolver>,
+ TestContainer.IResolver>,
+ TestContainer.IResolver,
+ TestContainer.IResolver,
+ TestContainer.IResolver,
+ TestContainer.IResolver,
+ TestContainer.IResolver
+
+ {
+ private readonly object _lock = new object();
+ private global::System.Collections.Generic.List? _disposables;
+ private readonly TestContainer _root;
+ public Scope(TestContainer root) => _root = root;
+ public global::System.IServiceProvider ServiceProvider => this;
+
+ private global::IPaymentGateway S0() => _root.S0();
+ private volatile global::ILedger? _S1;
+ private global::ILedger S1()
+ {
+ var service = _S1;
+ if (service is not null)
+ return service;
+ lock (_lock)
+ return (_S1 ??= Track(global::PaymentsModule.CreateLedger(S2())));
+ }
+ private global::IPaymentGateway S2() => _root.S2();
+
+ private object? Resolve(global::System.Type type, object? key, bool probe)
+ {
+ if (key is null)
+ {
+ if (type == typeof(global::IPaymentGateway))
+ return probe ? this : (object)(S2());
+ if (type == typeof(global::ILedger))
+ return probe ? this : (object)(S1());
+ if (type == typeof(global::System.Collections.Generic.IEnumerable))
+ return probe ? this : (object)(new global::IPaymentGateway[] { S0(), S2() });
+ if (type == typeof(global::System.Collections.Generic.IEnumerable))
+ return probe ? this : (object)(new global::ILedger[] { S1() });
+ if (type == typeof(global::System.IServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider))
+ return probe ? this : (object)(this);
+ if (type == typeof(global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService))
+ return probe ? this : (object)(this);
+ return null;
+ }
+ return null;
+ }
+
+ private global::System.IServiceProvider? Fallback => null;
+
+ public object? GetService(global::System.Type serviceType)
+ => GetKeyedService(serviceType, null);
+
+ public object? GetKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, false)
+ ?? (serviceKey == null ? Fallback?.GetService(serviceType) : (Fallback as global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider)?.GetKeyedService(serviceType, serviceKey));
+
+ public object GetRequiredKeyedService(global::System.Type serviceType, object? serviceKey)
+ => GetKeyedService(serviceType, serviceKey)
+ ?? throw new global::System.InvalidOperationException("No service for type '" + serviceType + "' has been registered.");
+
+ public T? GetService()
+ => this is TestContainer.IResolver resolver ? resolver.Get() : GetService(typeof(T)) is T service ? service : default;
+
+ public T GetRequiredService()
+ => this is TestContainer.IResolver resolver ? resolver.Get() : (T)GetRequiredKeyedService(typeof(T), null);
+
+ global::IPaymentGateway TestContainer.IResolver.Get()
+ => S2();
+ global::ILedger TestContainer.IResolver.Get()
+ => S1();
+ global::System.Collections.Generic.IEnumerable TestContainer.IResolver>.Get()
+ => new global::IPaymentGateway[] { S0(), S2() };
+ global::System.Collections.Generic.IEnumerable TestContainer.IResolver>.Get()
+ => new global::ILedger[] { S1() };
+ global::System.IServiceProvider TestContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory TestContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService TestContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider TestContainer.IResolver.Get()
+ => this;
+ global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService TestContainer.IResolver.Get()
+ => this;
+
+ public bool IsService(global::System.Type serviceType)
+ => IsKeyedService(serviceType, null);
+
+ public bool IsKeyedService(global::System.Type serviceType, object? serviceKey)
+ => Resolve(serviceType, serviceKey, true) != null || (serviceKey == null ? Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsService s && s.IsService(serviceType) : Fallback is global::Microsoft.Extensions.DependencyInjection.IServiceProviderIsKeyedService k && k.IsKeyedService(serviceType, serviceKey));
+
+ public Scope CreateScope() => new Scope(_root);
+ global::Microsoft.Extensions.DependencyInjection.IServiceScope global::Microsoft.Extensions.DependencyInjection.IServiceScopeFactory.CreateScope() => CreateScope();
+
+ private static T Required(object? service)
+ => service is T t ? t : throw new global::System.InvalidOperationException("No service for type '" + typeof(T) + "' has been registered.");
+
+ private T Track(T service)
+ {
+ if (service is global::System.IDisposable)
+ lock (_lock)
+ (_disposables ??= new global::System.Collections.Generic.List()).Add(service);
+ return service;
+ }
+
+ private object[] Drain()
+ {
+ lock (_lock)
+ {
+ var items = _disposables?.ToArray() ?? new object[0];
+ _disposables = null;
+ global::System.Array.Reverse(items);
+ return items;
+ }
+ }
+
+ public void Dispose()
+ {
+ foreach (var item in Drain())
+ {
+ if (item is global::System.IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+ }
+ }
+}
+```
+snippet source | anchor
+
+
+
+
+A module can live in another assembly, but the registration attributes are `[Conditional]`, so that assembly has to define `AUTOCTOR_USAGES` (see [Keeping Attributes In Code](#keeping-attributes-in-code)) for them to be there to read.
+
Back to Contents
## Other
@@ -583,6 +2515,8 @@ Your project file should look like this:
```
+The generic forms of `[Singleton]`, `[Transient]` and `[Scoped]` are a C# 11 feature, so they are not embedded by default. If your project targets C# 11 or later and you want them, also define `AUTOCTOR_EMBED_GENERIC_ATTRIBUTES`.
+
What gets generated
@@ -598,7 +2532,6 @@ Your project file should look like this:
#if AUTOCTOR_EMBED_ATTRIBUTES
namespace AutoCtor
{
- [global::System.Runtime.CompilerServices.CompilerGenerated]
[global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
internal enum GuardSetting
{
@@ -606,47 +2539,124 @@ namespace AutoCtor
Disabled,
Enabled
}
- [global::System.Runtime.CompilerServices.CompilerGenerated]
[global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCode]
- [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
internal sealed class AutoConstructAttribute : global::System.Attribute
{
- public AutoConstructAttribute(GuardSetting guard = GuardSetting.Default)
- {
- }
+ public AutoConstructAttribute(GuardSetting guard = GuardSetting.Default) { }
}
- [global::System.Runtime.CompilerServices.CompilerGenerated]
[global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCode]
- [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
internal sealed class AutoPostConstructAttribute : global::System.Attribute
{
}
- [global::System.Runtime.CompilerServices.CompilerGenerated]
[global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCode]
- [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.AttributeUsage(global::System.AttributeTargets.Field | global::System.AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
internal sealed class AutoConstructIgnoreAttribute : global::System.Attribute
{
}
- [global::System.Runtime.CompilerServices.CompilerGenerated]
[global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
- [global::System.Diagnostics.DebuggerNonUserCode]
- [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
[global::System.AttributeUsage(global::System.AttributeTargets.Field | global::System.AttributeTargets.Property | global::System.AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
internal sealed class AutoKeyedServiceAttribute : global::System.Attribute
{
public object Key { get; }
public AutoKeyedServiceAttribute(object key) => Key = key;
}
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+ internal sealed class ServiceProviderAttribute : global::System.Attribute
+ {
+ public string Fallback { get; set; }
+ }
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ internal abstract class ServiceAttribute : global::System.Attribute
+ {
+ public object Key { get; set; }
+ public string Factory { get; set; }
+ }
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ internal sealed class SingletonAttribute : ServiceAttribute
+ {
+ public SingletonAttribute(global::System.Type service, global::System.Type implementation = null) { }
+ }
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ internal sealed class TransientAttribute : ServiceAttribute
+ {
+ public TransientAttribute(global::System.Type service, global::System.Type implementation = null) { }
+ }
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ internal sealed class ScopedAttribute : ServiceAttribute
+ {
+ public ScopedAttribute(global::System.Type service, global::System.Type implementation = null) { }
+ }
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ internal sealed class ImportAttribute : global::System.Attribute
+ {
+ public ImportAttribute(global::System.Type module) { }
+ }
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ [global::System.Flags]
+ internal enum ScanAs
+ {
+ Service = 1,
+ Self = 2,
+ ImplementedInterfaces = 4
+ }
+ internal abstract class ScanAttribute : global::System.Attribute
+ {
+ public ScanAs As { get; set; }
+ public global::System.Type[] FromAssembliesOf { get; set; }
+ public string TypeNameFilter { get; set; }
+ }
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ internal sealed class ScanSingletonAttribute : ScanAttribute
+ {
+ public ScanSingletonAttribute(global::System.Type service = null) { }
+ }
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ internal sealed class ScanTransientAttribute : ScanAttribute
+ {
+ public ScanTransientAttribute(global::System.Type service = null) { }
+ }
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ internal sealed class ScanScopedAttribute : ScanAttribute
+ {
+ public ScanScopedAttribute(global::System.Type service = null) { }
+ }
+ #if AUTOCTOR_EMBED_GENERIC_ATTRIBUTES
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ internal sealed class SingletonAttribute : ServiceAttribute { }
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ internal sealed class SingletonAttribute : ServiceAttribute { }
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ internal sealed class TransientAttribute : ServiceAttribute { }
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ internal sealed class TransientAttribute : ServiceAttribute { }
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ internal sealed class ScopedAttribute : ServiceAttribute { }
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ internal sealed class ScopedAttribute : ServiceAttribute { }
+ [global::System.CodeDom.Compiler.GeneratedCode("AutoCtor", "0.0.0.0")]
+ [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ internal sealed class ImportAttribute : global::System.Attribute { }
+ #endif
}
#endif
```
-snippet source | anchor
+snippet source | anchor
@@ -669,10 +2679,3 @@ If you wish to preserve these attributes in the build output, add the define con
```
Back to Contents
-## Star History
-
-[](https://www.star-history.com/#distantcam/autoctor&type=date&legend=top-left)
-
-## Stats
-
-
diff --git a/readme.nuget.md b/readme.nuget.md
index 527032d0..3373a6cd 100644
--- a/readme.nuget.md
+++ b/readme.nuget.md
@@ -37,3 +37,23 @@ public partial class AService
- }
}
```
+
+# Service Providers
+
+AutoCtor can also build the container. Register services with attributes on a partial class, and the whole object graph is resolved during compilation and emitted as plain C#.
+
+```c#
+[ServiceProvider]
+[Singleton]
+[Scoped]
+[Transient]
+public partial class Container;
+```
+
+You get a real `IServiceProvider` with scopes, and disposal in reverse construction order. There is no reflection, no container and no registration API at run time -- every construction site is a literal `new`, so there is nothing for trimming or AOT to preserve.
+
+Because the graph is decided when the compiler runs, the problems Microsoft's container finds when someone resolves a service are build errors instead: a dependency nothing is registered for, a circular dependency, a scoped service captured by a singleton.
+
+Singletons, scoped services, transients, keyed services, open generics, `IEnumerable` injection, factory members and `IAsyncDisposable` are all supported. AutoCtor adds no package dependency of its own: where `Microsoft.Extensions.DependencyInjection.Abstractions` is referenced the generated types implement the interfaces from it that fit, and where it is not, those interfaces are left off and every member behind them stands on its own. A provider can also fall back to another `IServiceProvider` for anything it does not register, which is how it sits under a host.
+
+See the [full readme](https://github.com/distantcam/AutoCtor#service-provider) for details.
diff --git a/readme.source.md b/readme.source.md
index 1d1cbaee..8a8fb47b 100644
--- a/readme.source.md
+++ b/readme.source.md
@@ -50,6 +50,10 @@ Starting in version 3.0, a new diagnostic has been introduced: `ACTR007 Use [Aut
In 3.1 another code fix was added: `Add [AutoConstruct] to type`. This will add `[AutoConstruct]` to a type with no constructor and relevant fields. This is useful when working on a new type.
+## Service Provider Source Generator (new in 4.0)
+
+In version 4.0 you can now generate your own Service Provider that will work with the constructors generated by AutoCtor. See [Service Provider](#service-provider)
+
## Examples
### Basic
@@ -171,6 +175,88 @@ snippet: Keyed.cs#Keyed.g.verified.cs
+Back to Contents
+## Service Provider
+
+snippet: ServiceProvider
+
+What gets generated
+
+snippet: ServiceProvider.cs#Container.ServiceProvider.g.verified.cs
+
+
+
+Add `[ServiceProvider]` to a blank partial class and register services with `[Singleton]`, `[Transient]` or `[Scoped]` attributes.
+
+The service provider is fully generated at compile time so there is no runtime reflection.
+
+Multiple registrations for the same service can be returned with `IEnumerable`. When getting just one the last registration wins.
+
+
+### Scanning
+
+As well as registering services individually there is also an option to scan for services. Scanning is performed at compile time.
+
+snippet: ServiceProviderScan
+
+What gets generated
+
+snippet: ServiceProviderScan.cs#OrderContainer.ServiceProvider.g.verified.cs
+
+
+
+The scan attributes `[ScanSingleton]`, `[ScanTransient]`, and `[ScanScoped]` have some options.
+
+- `As` determines what to register the service as: `ScanAs.Service`, the service being scanned for (default); `ScanAs.Self`, the type itself; `ScanAs.ImplementedInterfaces`, all the interfaces the type has.
+- `FromAssembliesOf` can be used to scan other assemblies instead of the one the provider is in; add the provider too to keep it in the list of assemblies to scan.
+- `TypeNameFilter` only includes types whose name matches, where `*` matches anything and `?` any one character, e.g. `"*Repository"`. The service can be left out to select by name alone.
+
+### Keyed Services
+
+snippet: ServiceProviderKeyed
+
+What gets generated
+
+snippet: ServiceProviderKeyed.cs#PaymentContainer.ServiceProvider.g.verified.cs
+
+
+
+### Factories
+
+snippet: ServiceProviderFactory
+
+What gets generated
+
+snippet: ServiceProviderFactory.cs#HostContainer.ServiceProvider.g.verified.cs
+
+
+
+### Falling back to another provider
+
+`Fallback` on `[ServiceProvider]` names a member to use when there is no registration for that service.
+
+snippet: ServiceProviderFallback
+
+What gets generated
+
+snippet: ServiceProviderFallback.cs#FallbackContainer.ServiceProvider.g.verified.cs
+
+
+
+### Modules
+
+`[Import]` (or `[Import(typeof(TModule))]`) adds all the registrations from another type. Modules just act as markers so no code is generated for them.
+
+snippet: ServiceProviderModule
+
+What gets generated
+
+snippet: ServiceProviderModule.cs#TestContainer.ServiceProvider.g.verified.cs
+
+
+
+A module can live in another assembly, but the registration attributes are `[Conditional]`, so that assembly has to define `AUTOCTOR_USAGES` (see [Keeping Attributes In Code](#keeping-attributes-in-code)) for them to be there to read.
+
Back to Contents
## Other
@@ -200,6 +286,8 @@ Your project file should look like this:
```
+The generic forms of `[Singleton]`, `[Transient]` and `[Scoped]` are a C# 11 feature, so they are not embedded by default. If your project targets C# 11 or later and you want them, also define `AUTOCTOR_EMBED_GENERIC_ATTRIBUTES`.
+
What gets generated
snippet: GeneratedAttributeTests.cs#AutoConstructAttribute.g.verified.cs
@@ -224,10 +312,3 @@ If you wish to preserve these attributes in the build output, add the define con
```
Back to Contents
-## Star History
-
-[](https://www.star-history.com/#distantcam/autoctor&type=date&legend=top-left)
-
-## Stats
-
-
diff --git a/src/AutoCtor.Attributes/IoCAttributes.cs b/src/AutoCtor.Attributes/IoCAttributes.cs
new file mode 100644
index 00000000..41de74a7
--- /dev/null
+++ b/src/AutoCtor.Attributes/IoCAttributes.cs
@@ -0,0 +1,134 @@
+using System.Diagnostics;
+using static System.AttributeTargets;
+
+namespace AutoCtor;
+
+///
+/// Generates a compile time from the registration attributes on this class.
+///
+[AttributeUsage(Class, Inherited = false)]
+[Conditional("AUTOCTOR_USAGES")]
+public sealed class ServiceProviderAttribute : Attribute
+{
+ ///
+ /// A field, property or parameterless method (named with nameof) returning an
+ /// to ask for anything not registered.
+ ///
+ public string? Fallback { get; set; }
+}
+
+public abstract class ServiceAttribute : Attribute
+{
+ public object? Key { get; set; }
+
+ ///
+ /// A method, property or field (named with nameof) that builds the service instead
+ /// of its constructor. A method's parameters are resolved like constructor parameters.
+ ///
+ public string? Factory { get; set; }
+}
+
+[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
+[Conditional("AUTOCTOR_USAGES")]
+public sealed class SingletonAttribute(Type service, Type? implementation = null) : ServiceAttribute
+{
+ public Type Service { get; } = service;
+ public Type? Implementation { get; } = implementation;
+}
+
+[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
+[Conditional("AUTOCTOR_USAGES")]
+public sealed class SingletonAttribute : ServiceAttribute;
+
+[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
+[Conditional("AUTOCTOR_USAGES")]
+public sealed class SingletonAttribute : ServiceAttribute;
+
+[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
+[Conditional("AUTOCTOR_USAGES")]
+public sealed class TransientAttribute(Type service, Type? implementation = null) : ServiceAttribute
+{
+ public Type Service { get; } = service;
+ public Type? Implementation { get; } = implementation;
+}
+
+[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
+[Conditional("AUTOCTOR_USAGES")]
+public sealed class TransientAttribute : ServiceAttribute;
+
+[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
+[Conditional("AUTOCTOR_USAGES")]
+public sealed class TransientAttribute : ServiceAttribute;
+
+[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
+[Conditional("AUTOCTOR_USAGES")]
+public sealed class ScopedAttribute(Type service, Type? implementation = null) : ServiceAttribute
+{
+ public Type Service { get; } = service;
+ public Type? Implementation { get; } = implementation;
+}
+
+[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
+[Conditional("AUTOCTOR_USAGES")]
+public sealed class ScopedAttribute : ServiceAttribute;
+
+[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
+[Conditional("AUTOCTOR_USAGES")]
+public sealed class ScopedAttribute : ServiceAttribute;
+
+///
+/// Adds every registration, scan and import on another class, in place. A module in another
+/// assembly is only seen when that assembly defines AUTOCTOR_USAGES.
+///
+[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
+[Conditional("AUTOCTOR_USAGES")]
+public sealed class ImportAttribute(Type module) : Attribute
+{
+ public Type Module { get; } = module;
+}
+
+///
+[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
+[Conditional("AUTOCTOR_USAGES")]
+public sealed class ImportAttribute : Attribute;
+
+[Flags]
+public enum ScanAs
+{
+ /// The scanned for type, closed over the match for an open generic.
+ Service = 1,
+ /// The implementation itself.
+ Self = 2,
+ /// Every interface of the implementation.
+ ImplementedInterfaces = 4,
+}
+
+///
+/// Registers every accessible, concrete, non-generic class assignable to ,
+/// and matching . At least one of the two is required.
+///
+public abstract class ScanAttribute(Type? service) : Attribute
+{
+ ///
+ /// An open generic such as typeof(IHandler<>) matches every construction of it.
+ /// Without one, registers each type as itself.
+ ///
+ public Type? Service { get; } = service;
+ public ScanAs As { get; set; } = ScanAs.Service;
+ /// Scans the assemblies of these types instead of the provider's own.
+ public Type[]? FromAssembliesOf { get; set; }
+ /// Only types whose name matches, where * matches anything and ? any one character, such as "*Repository".
+ public string? TypeNameFilter { get; set; }
+}
+
+[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
+[Conditional("AUTOCTOR_USAGES")]
+public sealed class ScanSingletonAttribute(Type? service = null) : ScanAttribute(service);
+
+[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
+[Conditional("AUTOCTOR_USAGES")]
+public sealed class ScanTransientAttribute(Type? service = null) : ScanAttribute(service);
+
+[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
+[Conditional("AUTOCTOR_USAGES")]
+public sealed class ScanScopedAttribute(Type? service = null) : ScanAttribute(service);
diff --git a/src/AutoCtor.Example/AutoCtor.Example.csproj b/src/AutoCtor.Example/AutoCtor.Example.csproj
deleted file mode 100644
index 7dae911e..00000000
--- a/src/AutoCtor.Example/AutoCtor.Example.csproj
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
- net10.0
- disable
- false
- CS0169
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/AutoCtor.Roslyn3.11/AutoConstructSourceGenerator.cs b/src/AutoCtor.Roslyn3.11/AutoConstructSourceGenerator.cs
index e1917de1..7f5b297d 100644
--- a/src/AutoCtor.Roslyn3.11/AutoConstructSourceGenerator.cs
+++ b/src/AutoCtor.Roslyn3.11/AutoConstructSourceGenerator.cs
@@ -9,6 +9,7 @@ private sealed class SyntaxContextReceiver(CancellationToken cancellationToken)
{
public List? TypeModels { get; private set; }
public List? MarkedMethods { get; private set; }
+ public List? ServiceProviders { get; private set; }
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
@@ -16,11 +17,13 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
IMethodSymbol? method;
if (GeneratorUtilities.IsTypeDeclarationWithAttributes(context.Node, cancellationToken)
- && (type = GeneratorUtilities.GetSymbol(context, cancellationToken)) != null
-
- && Utilities.HasAttribute(type, AttributeNames.AutoConstruct))
+ && (type = GeneratorUtilities.GetPrimarySymbol(context, cancellationToken)) != null)
{
- (TypeModels ??= []).Add(TypeModel.Create(type));
+ if (Utilities.HasAttribute(type, AttributeNames.AutoConstruct))
+ (TypeModels ??= []).Add(TypeModel.Create(type));
+
+ if (Utilities.HasAttribute(type, AttributeNames.ServiceProvider))
+ (ServiceProviders ??= []).Add(ServiceProviderModel.Create(type));
}
else if (GeneratorUtilities.IsMethodDeclarationWithAttributes(context.Node, cancellationToken)
@@ -43,7 +46,7 @@ public void Initialize(GeneratorInitializationContext context)
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxContextReceiver is not SyntaxContextReceiver receiver
- || receiver.TypeModels == null)
+ || (receiver.TypeModels == null && receiver.ServiceProviders == null))
return;
var enableGuards = false;
@@ -55,10 +58,11 @@ public void Execute(GeneratorExecutionContext context)
projectGuardSetting.Equals("enable", StringComparison.OrdinalIgnoreCase);
}
- var models = (
- receiver.TypeModels.ToImmutableArray(),
- receiver.MarkedMethods?.ToImmutableArray() ?? ImmutableArray.Empty
- );
- Emitter.GenerateSource(context, (models, enableGuards));
+ Emitter.GenerateSource(context, (
+ receiver.TypeModels?.ToImmutableArray() ?? ImmutableArray.Empty,
+ receiver.MarkedMethods?.ToImmutableArray() ?? ImmutableArray.Empty,
+ enableGuards,
+ receiver.ServiceProviders?.ToImmutableArray() ?? ImmutableArray.Empty,
+ DuckTypes.Create(context.Compilation)));
}
}
diff --git a/src/AutoCtor.Roslyn4.0/AutoConstructSourceGenerator.cs b/src/AutoCtor.Roslyn4.0/AutoConstructSourceGenerator.cs
index 15d2f473..0bd0bf99 100644
--- a/src/AutoCtor.Roslyn4.0/AutoConstructSourceGenerator.cs
+++ b/src/AutoCtor.Roslyn4.0/AutoConstructSourceGenerator.cs
@@ -16,7 +16,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var types = context.SyntaxProvider.CreateSyntaxProvider(
GeneratorUtilities.IsTypeDeclarationWithAttributes,
- GeneratorUtilities.GetSymbol)
+ GeneratorUtilities.GetPrimarySymbol)
.Where(static x => Utilities.HasAttribute(x, AttributeNames.AutoConstruct))
.Select(static (x, _) => TypeModel.Create(x!))
.Collect();
@@ -28,8 +28,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.Select(static (x, _) => PostCtorModel.Create(x!))
.Collect();
+ var serviceProviders = context.SyntaxProvider.CreateSyntaxProvider(
+ GeneratorUtilities.IsTypeDeclarationWithAttributes,
+ GeneratorUtilities.GetPrimarySymbol)
+ .Where(static x => Utilities.HasAttribute(x, AttributeNames.ServiceProvider))
+ .Select(static (x, _) => ServiceProviderModel.Create(x!))
+ .Collect();
+
+ var duckTypes = context.CompilationProvider.Select(static (c, ct) => DuckTypes.Create(c));
+
context.RegisterSourceOutput(
- types.Combine(postCtorMethods).Combine(properties),
+ types.Combine(postCtorMethods).Combine(properties).Combine(serviceProviders).Combine(duckTypes)
+ .Select(static (x, _) => (x.Left.Left.Left.Left, x.Left.Left.Left.Right, x.Left.Left.Right, x.Left.Right, x.Right)),
Emitter.GenerateSource);
}
}
diff --git a/src/AutoCtor.Roslyn4.4/AutoConstructSourceGenerator.cs b/src/AutoCtor.Roslyn4.4/AutoConstructSourceGenerator.cs
index a0cbe3c0..07834385 100644
--- a/src/AutoCtor.Roslyn4.4/AutoConstructSourceGenerator.cs
+++ b/src/AutoCtor.Roslyn4.4/AutoConstructSourceGenerator.cs
@@ -29,8 +29,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.WithTrackingName(TrackingNames.PostCtorMethods)
.Collect();
+ var serviceProviders = context.SyntaxProvider.ForAttributeWithMetadataName(
+ AttributeNames.ServiceProvider,
+ GeneratorUtilities.IsTypeDeclarationWithAttributes,
+ static (c, ct) => ServiceProviderModel.Create((INamedTypeSymbol)c.TargetSymbol))
+ .WithTrackingName(TrackingNames.ServiceProviders)
+ .Collect();
+
+ var duckTypes = context.CompilationProvider.Select(static (c, ct) => DuckTypes.Create(c));
+
context.RegisterSourceOutput(
- types.Combine(postCtorMethods).Combine(properties),
+ types.Combine(postCtorMethods).Combine(properties).Combine(serviceProviders).Combine(duckTypes)
+ .Select(static (x, _) => (x.Left.Left.Left.Left, x.Left.Left.Left.Right, x.Left.Left.Right, x.Left.Right, x.Right)),
Emitter.GenerateSource);
}
}
diff --git a/src/Benchmarks/AutoCtorBenchmarks.cs b/src/Benchmarks/AutoCtorBenchmarks.cs
deleted file mode 100644
index e318c66c..00000000
--- a/src/Benchmarks/AutoCtorBenchmarks.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-using AutoCtor;
-using BenchmarkDotNet.Attributes;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.Testing;
-
-namespace AutoCtor.Benchmarks;
-
-[MemoryDiagnoser]
-public class AutoCtorBenchmarks
-{
- private static IReadOnlyList? s_references;
-
- private CSharpCompilation _compilation = null!;
- private GeneratorDriver _warmDriver = null!;
-
- [Params(100, 500, 1000)]
- public int FileCount { get; set; }
-
- [Params(5, 10)]
- public int FieldCount { get; set; }
-
- [GlobalSetup]
- public async Task Setup()
- {
- s_references ??= (await ReferenceAssemblies.NetStandard.NetStandard20
- .ResolveAsync(LanguageNames.CSharp, CancellationToken.None)
- .ConfigureAwait(false))
- .Add(MetadataReference.CreateFromFile(typeof(AutoConstructAttribute).Assembly.Location));
-
- _compilation = CSharpCompilation.Create(
- "BenchmarkAssembly",
- Enumerable.Range(0, FileCount)
- .Select(i => CSharpSyntaxTree.ParseText(BuildCode(i, FieldCount)))
- .ToArray(),
- s_references,
- new CSharpCompilationOptions(
- OutputKind.DynamicallyLinkedLibrary,
- nullableContextOptions: NullableContextOptions.Enable));
-
- _warmDriver = CreateDriver().RunGenerators(_compilation);
- }
-
- [Benchmark(Baseline = true)]
- public GeneratorDriverRunResult Cold()
- {
- return CreateDriver().RunGenerators(_compilation).GetRunResult();
- }
-
- [Benchmark]
- public GeneratorDriverRunResult Cached()
- {
- _warmDriver = _warmDriver.RunGenerators(_compilation);
- return _warmDriver.GetRunResult();
- }
-
- private static CSharpGeneratorDriver CreateDriver()
- {
- return CSharpGeneratorDriver.Create(
- [new AutoConstructSourceGenerator().AsSourceGenerator()],
- parseOptions: CSharpParseOptions.Default);
- }
-
- private static string BuildCode(int fileIndex, int fieldCount)
- {
- var interfaces = string.Join(
- "\n",
- Enumerable.Range(1, fieldCount)
- .Select(i => $"public interface IService{fileIndex}_{i} {{ }}"));
-
- var fields = string.Join(
- "\n",
- Enumerable.Range(1, fieldCount)
- .Select(i => $" private readonly IService{fileIndex}_{i} _service{i};"));
-
- return $$"""
- using AutoCtor;
-
- {{interfaces}}
-
- [AutoConstruct]
- public partial class TestService{{fileIndex}}
- {
- {{fields}}
- }
- """;
- }
-}
diff --git a/src/Benchmarks/BasicComplexBenchmark.cs b/src/Benchmarks/BasicComplexBenchmark.cs
new file mode 100644
index 00000000..ef4b98de
--- /dev/null
+++ b/src/Benchmarks/BasicComplexBenchmark.cs
@@ -0,0 +1,118 @@
+using BenchmarkDotNet.Attributes;
+using Jab;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Benchmarks;
+
+[BenchmarkCategory(Categories.Complex)]
+[ShortRunJob]
+[MemoryDiagnoser]
+public partial class BasicComplexBenchmark
+{
+ // One loop length. The 100 and 1000 runs measured the same per-resolve cost, and
+ // resolving one, two or three services differed from each other by a multiplier.
+ private const int GetCount = 1000;
+
+ private readonly ServiceProvider _provider;
+ private readonly JabComplexProvider _jabProvider = new();
+ private readonly AutoCtorProvider _autoCtorProvider = new();
+
+ public BasicComplexBenchmark()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddScoped();
+ serviceCollection.AddScoped();
+ serviceCollection.AddScoped();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ _provider = serviceCollection.BuildServiceProvider();
+ }
+
+ [BenchmarkCategory(Categories.MEDI)]
+ [Benchmark]
+ public void MEDI()
+ {
+ for (var i = 0; i < GetCount; i++)
+ {
+ using var scope = _provider.CreateScope();
+
+ scope.ServiceProvider.GetService();
+ scope.ServiceProvider.GetService();
+ scope.ServiceProvider.GetService();
+ }
+ }
+
+ [BenchmarkCategory(Categories.Jab)]
+ [Benchmark(Baseline = true)]
+ public void Jab()
+ {
+ for (var i = 0; i < GetCount; i++)
+ {
+ using var scope = _jabProvider.CreateScope();
+
+ scope.GetService();
+ scope.GetService();
+ scope.GetService();
+ }
+ }
+
+ [BenchmarkCategory(Categories.AutoCtor)]
+ [Benchmark]
+ public void AutoCtor()
+ {
+ for (var i = 0; i < GetCount; i++)
+ {
+ using var scope = _autoCtorProvider.CreateScope();
+
+ scope.GetService();
+ scope.GetService();
+ scope.GetService();
+ }
+ }
+
+ [ServiceProvider]
+ [Scoped(typeof(IComplex1), typeof(Complex1))]
+ [Scoped(typeof(IComplex2), typeof(Complex2))]
+ [Scoped(typeof(IComplex3), typeof(Complex3))]
+ [Transient(typeof(IService1), typeof(Service1))]
+ [Transient(typeof(IService2), typeof(Service2))]
+ [Transient(typeof(IService3), typeof(Service3))]
+ [Transient(typeof(IMix1), typeof(Mix1))]
+ [Transient(typeof(IMix2), typeof(Mix2))]
+ [Transient(typeof(IMix3), typeof(Mix3))]
+ [Transient(typeof(ITransient1), typeof(Transient1))]
+ [Transient(typeof(ITransient2), typeof(Transient2))]
+ [Transient(typeof(ITransient3), typeof(Transient3))]
+ [Singleton(typeof(ISingleton1), typeof(Singleton1))]
+ [Singleton(typeof(ISingleton2), typeof(Singleton2))]
+ [Singleton(typeof(ISingleton3), typeof(Singleton3))]
+ private sealed partial class JabComplexProvider;
+
+ [AutoCtor.ServiceProvider]
+ [AutoCtor.Scoped(typeof(IComplex1), typeof(Complex1))]
+ [AutoCtor.Scoped(typeof(IComplex2), typeof(Complex2))]
+ [AutoCtor.Scoped(typeof(IComplex3), typeof(Complex3))]
+ [AutoCtor.Transient(typeof(IService1), typeof(Service1))]
+ [AutoCtor.Transient(typeof(IService2), typeof(Service2))]
+ [AutoCtor.Transient(typeof(IService3), typeof(Service3))]
+ [AutoCtor.Transient(typeof(IMix1), typeof(Mix1))]
+ [AutoCtor.Transient(typeof(IMix2), typeof(Mix2))]
+ [AutoCtor.Transient(typeof(IMix3), typeof(Mix3))]
+ [AutoCtor.Transient(typeof(ITransient1), typeof(Transient1))]
+ [AutoCtor.Transient(typeof(ITransient2), typeof(Transient2))]
+ [AutoCtor.Transient(typeof(ITransient3), typeof(Transient3))]
+ [AutoCtor.Singleton(typeof(ISingleton1), typeof(Singleton1))]
+ [AutoCtor.Singleton(typeof(ISingleton2), typeof(Singleton2))]
+ [AutoCtor.Singleton(typeof(ISingleton3), typeof(Singleton3))]
+ private sealed partial class AutoCtorProvider;
+}
diff --git a/src/Benchmarks/BasicMixedBenchmark.cs b/src/Benchmarks/BasicMixedBenchmark.cs
new file mode 100644
index 00000000..65e09e25
--- /dev/null
+++ b/src/Benchmarks/BasicMixedBenchmark.cs
@@ -0,0 +1,100 @@
+using BenchmarkDotNet.Attributes;
+using Jab;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Benchmarks;
+
+[BenchmarkCategory(Categories.Mixed)]
+[ShortRunJob]
+[MemoryDiagnoser]
+public partial class BasicMixedBenchmark
+{
+ // One loop length. The 100 and 1000 runs measured the same per-resolve cost, and
+ // resolving one, two or three services differed from each other by a multiplier.
+ private const int GetCount = 1000;
+
+ private readonly ServiceProvider _provider;
+ private readonly JabMixProvider _jabProvider = new();
+ private readonly AutoCtorProvider _autoCtorProvider = new();
+
+ public BasicMixedBenchmark()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ _provider = serviceCollection.BuildServiceProvider();
+ }
+
+ [BenchmarkCategory(Categories.MEDI)]
+ [Benchmark]
+ public void MEDI()
+ {
+ for (var i = 0; i < GetCount; i++)
+ {
+ using var scope = _provider.CreateScope();
+
+ scope.ServiceProvider.GetService();
+ scope.ServiceProvider.GetService();
+ scope.ServiceProvider.GetService();
+ }
+ }
+
+ [BenchmarkCategory(Categories.Jab)]
+ [Benchmark(Baseline = true)]
+ public void Jab()
+ {
+ for (var i = 0; i < GetCount; i++)
+ {
+ using var scope = _jabProvider.CreateScope();
+
+ scope.GetService();
+ scope.GetService();
+ scope.GetService();
+ }
+ }
+
+ [BenchmarkCategory(Categories.AutoCtor)]
+ [Benchmark]
+ public void AutoCtor()
+ {
+ for (var i = 0; i < GetCount; i++)
+ {
+ using var scope = _autoCtorProvider.CreateScope();
+
+ scope.GetService();
+ scope.GetService();
+ scope.GetService();
+ }
+ }
+
+ [ServiceProvider]
+ [Transient(typeof(IMix1), typeof(Mix1))]
+ [Transient(typeof(IMix2), typeof(Mix2))]
+ [Transient(typeof(IMix3), typeof(Mix3))]
+ [Transient(typeof(ITransient1), typeof(Transient1))]
+ [Transient(typeof(ITransient2), typeof(Transient2))]
+ [Transient(typeof(ITransient3), typeof(Transient3))]
+ [Singleton(typeof(ISingleton1), typeof(Singleton1))]
+ [Singleton(typeof(ISingleton2), typeof(Singleton2))]
+ [Singleton(typeof(ISingleton3), typeof(Singleton3))]
+ private sealed partial class JabMixProvider;
+
+ [AutoCtor.ServiceProvider]
+ [AutoCtor.Transient(typeof(IMix1), typeof(Mix1))]
+ [AutoCtor.Transient(typeof(IMix2), typeof(Mix2))]
+ [AutoCtor.Transient(typeof(IMix3), typeof(Mix3))]
+ [AutoCtor.Transient(typeof(ITransient1), typeof(Transient1))]
+ [AutoCtor.Transient(typeof(ITransient2), typeof(Transient2))]
+ [AutoCtor.Transient(typeof(ITransient3), typeof(Transient3))]
+ [AutoCtor.Singleton(typeof(ISingleton1), typeof(Singleton1))]
+ [AutoCtor.Singleton(typeof(ISingleton2), typeof(Singleton2))]
+ [AutoCtor.Singleton(typeof(ISingleton3), typeof(Singleton3))]
+ private sealed partial class AutoCtorProvider;
+}
diff --git a/src/Benchmarks/BasicScopedBenchmark.cs b/src/Benchmarks/BasicScopedBenchmark.cs
new file mode 100644
index 00000000..5a91fa1f
--- /dev/null
+++ b/src/Benchmarks/BasicScopedBenchmark.cs
@@ -0,0 +1,82 @@
+using BenchmarkDotNet.Attributes;
+using Jab;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Benchmarks;
+
+[BenchmarkCategory(Categories.Scoped)]
+[ShortRunJob]
+[MemoryDiagnoser]
+public partial class BasicScopedBenchmark
+{
+ // One loop length. The 100 and 1000 runs measured the same per-resolve cost, and
+ // resolving one, two or three services differed from each other by a multiplier.
+ private const int GetCount = 1000;
+
+ private readonly ServiceProvider _provider;
+ private readonly JabScopedProvider _jabProvider = new();
+ private readonly AutoCtorProvider _autoCtorProvider = new();
+
+ public BasicScopedBenchmark()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddScoped();
+ serviceCollection.AddScoped();
+ serviceCollection.AddScoped();
+ _provider = serviceCollection.BuildServiceProvider();
+ }
+
+ [BenchmarkCategory(Categories.MEDI)]
+ [Benchmark]
+ public void MEDI()
+ {
+ for (var i = 0; i < GetCount; i++)
+ {
+ using var scope = _provider.CreateScope();
+
+ scope.ServiceProvider.GetService();
+ scope.ServiceProvider.GetService();
+ scope.ServiceProvider.GetService();
+ }
+ }
+
+ [BenchmarkCategory(Categories.Jab)]
+ [Benchmark(Baseline = true)]
+ public void Jab()
+ {
+ for (var i = 0; i < GetCount; i++)
+ {
+ using var scope = _jabProvider.CreateScope();
+
+ scope.GetService();
+ scope.GetService();
+ scope.GetService();
+ }
+ }
+
+ [BenchmarkCategory(Categories.AutoCtor)]
+ [Benchmark]
+ public void AutoCtor()
+ {
+ for (var i = 0; i < GetCount; i++)
+ {
+ using var scope = _autoCtorProvider.CreateScope();
+
+ scope.GetService();
+ scope.GetService();
+ scope.GetService();
+ }
+ }
+
+ [ServiceProvider]
+ [Scoped(typeof(IScoped1), typeof(Scoped1))]
+ [Scoped(typeof(IScoped2), typeof(Scoped2))]
+ [Scoped(typeof(IScoped3), typeof(Scoped3))]
+ internal sealed partial class JabScopedProvider;
+
+ [AutoCtor.ServiceProvider]
+ [AutoCtor.Scoped(typeof(IScoped1), typeof(Scoped1))]
+ [AutoCtor.Scoped(typeof(IScoped2), typeof(Scoped2))]
+ [AutoCtor.Scoped(typeof(IScoped3), typeof(Scoped3))]
+ internal sealed partial class AutoCtorProvider;
+}
diff --git a/src/Benchmarks/BasicSingletonBenchmark.cs b/src/Benchmarks/BasicSingletonBenchmark.cs
new file mode 100644
index 00000000..2a916114
--- /dev/null
+++ b/src/Benchmarks/BasicSingletonBenchmark.cs
@@ -0,0 +1,76 @@
+using BenchmarkDotNet.Attributes;
+using Jab;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Benchmarks;
+
+[BenchmarkCategory(Categories.Singleton)]
+[ShortRunJob]
+[MemoryDiagnoser]
+public partial class BasicSingletonBenchmark
+{
+ // One loop length. The 100 and 1000 runs measured the same per-resolve cost, and
+ // resolving one, two or three services differed from each other by a multiplier.
+ private const int GetCount = 1000;
+
+ private readonly ServiceProvider _provider;
+ private readonly JabSingletonProvider _jabProvider = new();
+ private readonly AutoCtorProvider _autoCtorProvider = new();
+
+ public BasicSingletonBenchmark()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ _provider = serviceCollection.BuildServiceProvider();
+ }
+
+ [BenchmarkCategory(Categories.MEDI)]
+ [Benchmark]
+ public void MEDI()
+ {
+ for (var i = 0; i < GetCount; i++)
+ {
+ _provider.GetService();
+ _provider.GetService();
+ _provider.GetService();
+ }
+ }
+
+ [BenchmarkCategory(Categories.Jab)]
+ [Benchmark(Baseline = true)]
+ public void Jab()
+ {
+ for (var i = 0; i < GetCount; i++)
+ {
+ _jabProvider.GetService();
+ _jabProvider.GetService();
+ _jabProvider.GetService();
+ }
+ }
+
+ [BenchmarkCategory(Categories.AutoCtor)]
+ [Benchmark]
+ public void AutoCtor()
+ {
+ for (var i = 0; i < GetCount; i++)
+ {
+ _autoCtorProvider.GetService();
+ _autoCtorProvider.GetService();
+ _autoCtorProvider.GetService();
+ }
+ }
+
+ [ServiceProvider]
+ [Singleton(typeof(ISingleton1), typeof(Singleton1))]
+ [Singleton(typeof(ISingleton2), typeof(Singleton2))]
+ [Singleton(typeof(ISingleton3), typeof(Singleton3))]
+ private sealed partial class JabSingletonProvider;
+
+ [AutoCtor.ServiceProvider]
+ [AutoCtor.Singleton(typeof(ISingleton1), typeof(Singleton1))]
+ [AutoCtor.Singleton(typeof(ISingleton2), typeof(Singleton2))]
+ [AutoCtor.Singleton(typeof(ISingleton3), typeof(Singleton3))]
+ private sealed partial class AutoCtorProvider;
+}
diff --git a/src/Benchmarks/BasicTransientBenchmark.cs b/src/Benchmarks/BasicTransientBenchmark.cs
new file mode 100644
index 00000000..2f876377
--- /dev/null
+++ b/src/Benchmarks/BasicTransientBenchmark.cs
@@ -0,0 +1,76 @@
+using BenchmarkDotNet.Attributes;
+using Jab;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Benchmarks;
+
+[BenchmarkCategory(Categories.Transient)]
+[ShortRunJob]
+[MemoryDiagnoser]
+public partial class BasicTransientBenchmark
+{
+ // One loop length. The 100 and 1000 runs measured the same per-resolve cost, and
+ // resolving one, two or three services differed from each other by a multiplier.
+ private const int GetCount = 1000;
+
+ private readonly ServiceProvider _provider;
+ private readonly JabTransientProvider _jabProvider = new();
+ private readonly AutoCtorProvider _autoCtorProvider = new();
+
+ public BasicTransientBenchmark()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ _provider = serviceCollection.BuildServiceProvider();
+ }
+
+ [BenchmarkCategory(Categories.MEDI)]
+ [Benchmark]
+ public void MEDI()
+ {
+ for (var i = 0; i < GetCount; i++)
+ {
+ _provider.GetService();
+ _provider.GetService();
+ _provider.GetService();
+ }
+ }
+
+ [BenchmarkCategory(Categories.Jab)]
+ [Benchmark(Baseline = true)]
+ public void Jab()
+ {
+ for (var i = 0; i < GetCount; i++)
+ {
+ _jabProvider.GetService();
+ _jabProvider.GetService();
+ _jabProvider.GetService();
+ }
+ }
+
+ [BenchmarkCategory(Categories.AutoCtor)]
+ [Benchmark]
+ public void AutoCtor()
+ {
+ for (var i = 0; i < GetCount; i++)
+ {
+ _autoCtorProvider.GetService();
+ _autoCtorProvider.GetService();
+ _autoCtorProvider.GetService();
+ }
+ }
+
+ [ServiceProvider]
+ [Transient(typeof(ITransient1), typeof(Transient1))]
+ [Transient(typeof(ITransient2), typeof(Transient2))]
+ [Transient(typeof(ITransient3), typeof(Transient3))]
+ private sealed partial class JabTransientProvider;
+
+ [AutoCtor.ServiceProvider]
+ [AutoCtor.Transient(typeof(ITransient1), typeof(Transient1))]
+ [AutoCtor.Transient(typeof(ITransient2), typeof(Transient2))]
+ [AutoCtor.Transient(typeof(ITransient3), typeof(Transient3))]
+ private sealed partial class AutoCtorProvider;
+}
diff --git a/src/Benchmarks/AutoCtor.Benchmarks.csproj b/src/Benchmarks/Benchmarks.csproj
similarity index 73%
rename from src/Benchmarks/AutoCtor.Benchmarks.csproj
rename to src/Benchmarks/Benchmarks.csproj
index 7c43dc46..7ab5e483 100644
--- a/src/Benchmarks/AutoCtor.Benchmarks.csproj
+++ b/src/Benchmarks/Benchmarks.csproj
@@ -4,19 +4,21 @@
Exe
net10.0
false
- CA1515
+ default
+
+
-
+
diff --git a/src/Benchmarks/Categories.cs b/src/Benchmarks/Categories.cs
new file mode 100644
index 00000000..6c5179a8
--- /dev/null
+++ b/src/Benchmarks/Categories.cs
@@ -0,0 +1,14 @@
+namespace Benchmarks;
+
+internal class Categories
+{
+ public const string Singleton = "Singleton";
+ public const string Transient = "Transient";
+ public const string Scoped = "Scoped";
+ public const string Mixed = "Mixed";
+ public const string Complex = "Complex";
+
+ public const string MEDI = "MEDI";
+ public const string Jab = "Jab";
+ public const string AutoCtor = "AutoCtor";
+}
diff --git a/src/Benchmarks/Complex.cs b/src/Benchmarks/Complex.cs
new file mode 100644
index 00000000..5a717200
--- /dev/null
+++ b/src/Benchmarks/Complex.cs
@@ -0,0 +1,127 @@
+namespace Benchmarks;
+
+public interface IComplex1
+{
+ void Do1();
+}
+
+public interface IComplex2
+{
+ void Do2();
+}
+
+public interface IComplex3
+{
+ void Do3();
+}
+
+public interface IService1
+{
+ void Do1();
+}
+
+public interface IService2
+{
+ void Do2();
+}
+
+public interface IService3
+{
+ void Do3();
+}
+
+public class Service1(ITransient1 transient1) : IService1
+{
+ public void Do1()
+ {
+ transient1.Do1();
+ }
+}
+
+public class Service2(ITransient2 transient2) : IService2
+{
+ public void Do2()
+ {
+ transient2.Do2();
+ }
+}
+
+public class Service3(ITransient3 transient3) : IService3
+{
+ public void Do3()
+ {
+ transient3.Do3();
+ }
+}
+
+public class Complex1(
+ IService1 service1,
+ IService2 service2,
+ IService3 service3,
+ IMix1 mix1,
+ IMix2 mix2,
+ IMix3 mix3,
+ ISingleton1 singleton1,
+ ITransient1 transient1
+) : IComplex1
+{
+ public void Do1()
+ {
+ service1.Do1();
+ service2.Do2();
+ service3.Do3();
+ mix1.Do1();
+ mix2.Do2();
+ mix3.Do3();
+ singleton1.Do1();
+ transient1.Do1();
+ }
+}
+
+public class Complex2(
+ IService1 service1,
+ IService2 service2,
+ IService3 service3,
+ IMix1 mix1,
+ IMix2 mix2,
+ IMix3 mix3,
+ ISingleton2 singleton2,
+ ITransient2 transient2
+) : IComplex2
+{
+ public void Do2()
+ {
+ service1.Do1();
+ service2.Do2();
+ service3.Do3();
+ mix1.Do1();
+ mix2.Do2();
+ mix3.Do3();
+ singleton2.Do2();
+ transient2.Do2();
+ }
+}
+
+public class Complex3(
+ IService1 service1,
+ IService2 service2,
+ IService3 service3,
+ IMix1 mix1,
+ IMix2 mix2,
+ IMix3 mix3,
+ ISingleton3 singleton3,
+ ITransient3 transient3
+) : IComplex3
+{
+ public void Do3()
+ {
+ service1.Do1();
+ service2.Do2();
+ service3.Do3();
+ mix1.Do1();
+ mix2.Do2();
+ mix3.Do3();
+ singleton3.Do3();
+ transient3.Do3();
+ }
+}
diff --git a/src/Benchmarks/Mixed.cs b/src/Benchmarks/Mixed.cs
new file mode 100644
index 00000000..f57e4eb3
--- /dev/null
+++ b/src/Benchmarks/Mixed.cs
@@ -0,0 +1,43 @@
+namespace Benchmarks;
+
+public interface IMix1
+{
+ void Do1();
+}
+
+public interface IMix2
+{
+ void Do2();
+}
+
+public interface IMix3
+{
+ void Do3();
+}
+
+public class Mix1(ISingleton1 singleton1, ITransient1 transient1) : IMix1
+{
+ public void Do1()
+ {
+ singleton1.Do1();
+ transient1.Do1();
+ }
+}
+
+public class Mix2(ISingleton2 singleton2, ITransient2 transient2) : IMix2
+{
+ public void Do2()
+ {
+ singleton2.Do2();
+ transient2.Do2();
+ }
+}
+
+public class Mix3(ISingleton3 singleton3, ITransient3 transient3) : IMix3
+{
+ public void Do3()
+ {
+ singleton3.Do3();
+ transient3.Do3();
+ }
+}
diff --git a/src/Benchmarks/Program.cs b/src/Benchmarks/Program.cs
index 17a3237f..0b525519 100644
--- a/src/Benchmarks/Program.cs
+++ b/src/Benchmarks/Program.cs
@@ -1,8 +1,10 @@
-using AutoCtor.Benchmarks;
+using System.Reflection;
using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Order;
using BenchmarkDotNet.Running;
var config = ManualConfig.Create(DefaultConfig.Instance)
- .WithOptions(ConfigOptions.DisableOptimizationsValidator);
+ .WithOptions(ConfigOptions.DisableLogFile)
+ .WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest));
-BenchmarkRunner.Run(config, args);
+BenchmarkRunner.Run(Assembly.GetExecutingAssembly(), config);
diff --git a/src/Benchmarks/Scoped.cs b/src/Benchmarks/Scoped.cs
new file mode 100644
index 00000000..168271a3
--- /dev/null
+++ b/src/Benchmarks/Scoped.cs
@@ -0,0 +1,31 @@
+namespace Benchmarks;
+
+public interface IScoped1
+{
+ void Do1();
+}
+
+public interface IScoped2
+{
+ void Do2();
+}
+
+public interface IScoped3
+{
+ void Do3();
+}
+
+public class Scoped1 : IScoped1
+{
+ public void Do1() { }
+}
+
+public class Scoped2 : IScoped2
+{
+ public void Do2() { }
+}
+
+public class Scoped3 : IScoped3
+{
+ public void Do3() { }
+}
diff --git a/src/Benchmarks/Singletons.cs b/src/Benchmarks/Singletons.cs
new file mode 100644
index 00000000..5257978e
--- /dev/null
+++ b/src/Benchmarks/Singletons.cs
@@ -0,0 +1,31 @@
+namespace Benchmarks;
+
+public interface ISingleton1
+{
+ void Do1();
+}
+
+public interface ISingleton2
+{
+ void Do2();
+}
+
+public interface ISingleton3
+{
+ void Do3();
+}
+
+public class Singleton1 : ISingleton1
+{
+ public void Do1() { }
+}
+
+public class Singleton2 : ISingleton2
+{
+ public void Do2() { }
+}
+
+public class Singleton3 : ISingleton3
+{
+ public void Do3() { }
+}
diff --git a/src/Benchmarks/StartupBenchmark.cs b/src/Benchmarks/StartupBenchmark.cs
new file mode 100644
index 00000000..4cb3136a
--- /dev/null
+++ b/src/Benchmarks/StartupBenchmark.cs
@@ -0,0 +1,232 @@
+using BenchmarkDotNet.Attributes;
+using Jab;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Benchmarks;
+
+// What it costs to stand a container up, and nothing else. Every method returns the
+// provider rather than resolving from it: BenchmarkDotNet consumes the returned value, so
+// nothing is optimised away, and no service is constructed. Resolving anything here would
+// fold a lookup into the number -- and for AutoCtor and Jab that lookup is far larger than
+// the construction it would be hiding inside.
+[ShortRunJob]
+[MemoryDiagnoser]
+public partial class StartupBenchmark
+{
+ [BenchmarkCategory(Categories.Singleton)]
+ [Benchmark]
+ public IServiceProvider Jab_Singleton() => new JabStartupSingleton();
+
+ [BenchmarkCategory(Categories.Singleton)]
+ [Benchmark]
+ public IServiceProvider AutoCtor_Singleton() => new AutoCtorStartupSingleton();
+
+ [BenchmarkCategory(Categories.Singleton)]
+ [Benchmark]
+ public IServiceProvider MEDI_Singleton()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ return serviceCollection.BuildServiceProvider();
+ }
+
+ [ServiceProvider]
+ [Singleton(typeof(ISingleton1), typeof(Singleton1))]
+ [Singleton(typeof(ISingleton2), typeof(Singleton2))]
+ [Singleton(typeof(ISingleton3), typeof(Singleton3))]
+ private sealed partial class JabStartupSingleton;
+
+ [AutoCtor.ServiceProvider]
+ [AutoCtor.Singleton(typeof(ISingleton1), typeof(Singleton1))]
+ [AutoCtor.Singleton(typeof(ISingleton2), typeof(Singleton2))]
+ [AutoCtor.Singleton(typeof(ISingleton3), typeof(Singleton3))]
+ private sealed partial class AutoCtorStartupSingleton;
+
+ // ------------------------------------------------------------------------
+
+ [BenchmarkCategory(Categories.Scoped)]
+ [Benchmark]
+ public IServiceProvider Jab_Scoped() => new JabStartupScoped();
+
+ [BenchmarkCategory(Categories.Scoped)]
+ [Benchmark]
+ public IServiceProvider AutoCtor_Scoped() => new AutoCtorStartupScoped();
+
+ [BenchmarkCategory(Categories.Scoped)]
+ [Benchmark]
+ public IServiceProvider MEDI_Scoped()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddScoped();
+ serviceCollection.AddScoped();
+ serviceCollection.AddScoped();
+ return serviceCollection.BuildServiceProvider();
+ }
+
+ [ServiceProvider]
+ [Scoped(typeof(IScoped1), typeof(Scoped1))]
+ [Scoped(typeof(IScoped2), typeof(Scoped2))]
+ [Scoped(typeof(IScoped3), typeof(Scoped3))]
+ private partial class JabStartupScoped;
+
+ [AutoCtor.ServiceProvider]
+ [AutoCtor.Scoped(typeof(IScoped1), typeof(Scoped1))]
+ [AutoCtor.Scoped(typeof(IScoped2), typeof(Scoped2))]
+ [AutoCtor.Scoped(typeof(IScoped3), typeof(Scoped3))]
+ private sealed partial class AutoCtorStartupScoped;
+
+ // ------------------------------------------------------------------------
+
+ [BenchmarkCategory(Categories.Transient)]
+ [Benchmark]
+ public IServiceProvider Jab_Transient() => new JabStartupTransient();
+
+ [BenchmarkCategory(Categories.Transient)]
+ [Benchmark]
+ public IServiceProvider AutoCtor_Transient() => new AutoCtorStartupTransient();
+
+ [BenchmarkCategory(Categories.Transient)]
+ [Benchmark]
+ public IServiceProvider MEDI_Transient()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ return serviceCollection.BuildServiceProvider();
+ }
+
+ [ServiceProvider]
+ [Transient(typeof(ITransient1), typeof(Transient1))]
+ [Transient(typeof(ITransient2), typeof(Transient2))]
+ [Transient(typeof(ITransient3), typeof(Transient3))]
+ private partial class JabStartupTransient;
+
+ [AutoCtor.ServiceProvider]
+ [AutoCtor.Transient(typeof(ITransient1), typeof(Transient1))]
+ [AutoCtor.Transient(typeof(ITransient2), typeof(Transient2))]
+ [AutoCtor.Transient(typeof(ITransient3), typeof(Transient3))]
+ private sealed partial class AutoCtorStartupTransient;
+
+ // ------------------------------------------------------------------------
+
+ [BenchmarkCategory(Categories.Mixed)]
+ [Benchmark]
+ public IServiceProvider Jab_Mixed() => new JabStartupMixed();
+
+ [BenchmarkCategory(Categories.Mixed)]
+ [Benchmark]
+ public IServiceProvider AutoCtor_Mixed() => new AutoCtorStartupMixed();
+
+ [BenchmarkCategory(Categories.Mixed)]
+ [Benchmark]
+ public IServiceProvider MEDI_Mixed()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ return serviceCollection.BuildServiceProvider();
+ }
+
+ [ServiceProvider]
+ [Transient(typeof(IMix1), typeof(Mix1))]
+ [Transient(typeof(IMix2), typeof(Mix2))]
+ [Transient(typeof(IMix3), typeof(Mix3))]
+ [Transient(typeof(ITransient1), typeof(Transient1))]
+ [Transient(typeof(ITransient2), typeof(Transient2))]
+ [Transient(typeof(ITransient3), typeof(Transient3))]
+ [Singleton(typeof(ISingleton1), typeof(Singleton1))]
+ [Singleton(typeof(ISingleton2), typeof(Singleton2))]
+ [Singleton(typeof(ISingleton3), typeof(Singleton3))]
+ private partial class JabStartupMixed;
+
+ [AutoCtor.ServiceProvider]
+ [AutoCtor.Transient(typeof(IMix1), typeof(Mix1))]
+ [AutoCtor.Transient(typeof(IMix2), typeof(Mix2))]
+ [AutoCtor.Transient(typeof(IMix3), typeof(Mix3))]
+ [AutoCtor.Transient(typeof(ITransient1), typeof(Transient1))]
+ [AutoCtor.Transient(typeof(ITransient2), typeof(Transient2))]
+ [AutoCtor.Transient(typeof(ITransient3), typeof(Transient3))]
+ [AutoCtor.Singleton(typeof(ISingleton1), typeof(Singleton1))]
+ [AutoCtor.Singleton(typeof(ISingleton2), typeof(Singleton2))]
+ [AutoCtor.Singleton(typeof(ISingleton3), typeof(Singleton3))]
+ private sealed partial class AutoCtorStartupMixed;
+
+ // ------------------------------------------------------------------------
+
+ [BenchmarkCategory(Categories.Complex)]
+ [Benchmark]
+ public IServiceProvider Jab_Complex() => new JabStartupComplex();
+
+ [BenchmarkCategory(Categories.Complex)]
+ [Benchmark]
+ public IServiceProvider AutoCtor_Complex() => new AutoCtorStartupComplex();
+
+ [BenchmarkCategory(Categories.Complex)]
+ [Benchmark]
+ public IServiceProvider MEDI_Complex()
+ {
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddScoped();
+ serviceCollection.AddScoped();
+ serviceCollection.AddScoped();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ serviceCollection.AddSingleton();
+ return serviceCollection.BuildServiceProvider();
+ }
+
+ [ServiceProvider]
+ [Scoped(typeof(IComplex1), typeof(Complex1))]
+ [Scoped(typeof(IComplex2), typeof(Complex2))]
+ [Scoped(typeof(IComplex3), typeof(Complex3))]
+ [Transient(typeof(IService1), typeof(Service1))]
+ [Transient(typeof(IService2), typeof(Service2))]
+ [Transient(typeof(IService3), typeof(Service3))]
+ [Transient(typeof(IMix1), typeof(Mix1))]
+ [Transient(typeof(IMix2), typeof(Mix2))]
+ [Transient(typeof(IMix3), typeof(Mix3))]
+ [Transient(typeof(ITransient1), typeof(Transient1))]
+ [Transient(typeof(ITransient2), typeof(Transient2))]
+ [Transient(typeof(ITransient3), typeof(Transient3))]
+ [Singleton(typeof(ISingleton1), typeof(Singleton1))]
+ [Singleton(typeof(ISingleton2), typeof(Singleton2))]
+ [Singleton(typeof(ISingleton3), typeof(Singleton3))]
+ private partial class JabStartupComplex;
+
+ [AutoCtor.ServiceProvider]
+ [AutoCtor.Scoped(typeof(IComplex1), typeof(Complex1))]
+ [AutoCtor.Scoped(typeof(IComplex2), typeof(Complex2))]
+ [AutoCtor.Scoped(typeof(IComplex3), typeof(Complex3))]
+ [AutoCtor.Transient(typeof(IService1), typeof(Service1))]
+ [AutoCtor.Transient(typeof(IService2), typeof(Service2))]
+ [AutoCtor.Transient(typeof(IService3), typeof(Service3))]
+ [AutoCtor.Transient(typeof(IMix1), typeof(Mix1))]
+ [AutoCtor.Transient(typeof(IMix2), typeof(Mix2))]
+ [AutoCtor.Transient(typeof(IMix3), typeof(Mix3))]
+ [AutoCtor.Transient(typeof(ITransient1), typeof(Transient1))]
+ [AutoCtor.Transient(typeof(ITransient2), typeof(Transient2))]
+ [AutoCtor.Transient(typeof(ITransient3), typeof(Transient3))]
+ [AutoCtor.Singleton(typeof(ISingleton1), typeof(Singleton1))]
+ [AutoCtor.Singleton(typeof(ISingleton2), typeof(Singleton2))]
+ [AutoCtor.Singleton(typeof(ISingleton3), typeof(Singleton3))]
+ private sealed partial class AutoCtorStartupComplex;
+}
diff --git a/src/Benchmarks/Transient.cs b/src/Benchmarks/Transient.cs
new file mode 100644
index 00000000..53163f57
--- /dev/null
+++ b/src/Benchmarks/Transient.cs
@@ -0,0 +1,31 @@
+namespace Benchmarks;
+
+public interface ITransient1
+{
+ void Do1();
+}
+
+public interface ITransient2
+{
+ void Do2();
+}
+
+public interface ITransient3
+{
+ void Do3();
+}
+
+public class Transient1 : ITransient1
+{
+ public void Do1() { }
+}
+
+public class Transient2 : ITransient2
+{
+ public void Do2() { }
+}
+
+public class Transient3 : ITransient3
+{
+ public void Do3() { }
+}
diff --git a/src/CompileBenchmarks/CompileBenchmarks.csproj b/src/CompileBenchmarks/CompileBenchmarks.csproj
new file mode 100644
index 00000000..74d5f896
--- /dev/null
+++ b/src/CompileBenchmarks/CompileBenchmarks.csproj
@@ -0,0 +1,35 @@
+
+
+
+ Exe
+ net10.0
+ false
+ default
+
+
+
+ local
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CompileBenchmarks/LocalCompare.cs b/src/CompileBenchmarks/LocalCompare.cs
new file mode 100644
index 00000000..26d10402
--- /dev/null
+++ b/src/CompileBenchmarks/LocalCompare.cs
@@ -0,0 +1,85 @@
+using System.Text;
+using AutoCtor;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Jobs;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Testing;
+
+namespace CompileBenchmarks;
+
+[Config(typeof(Config))]
+public class LocalCompare
+{
+ [Params(100, 1000)]
+ public int ServiceCount { get; set; }
+
+ [Params(100, 1000)]
+ public int TargetsCount { get; set; }
+
+ private class Config : ManualConfig
+ {
+ public Config()
+ {
+ AddJob(Job.ShortRun
+ .WithMsBuildArguments("/p:AutoCtorVersion=local")
+ .WithId("local"));
+ AddJob(Job.ShortRun
+ .WithMsBuildArguments("/p:AutoCtorVersion=3.1.1")
+ .WithId("3.1.1")
+ .AsBaseline());
+
+ HideColumns(Column.Arguments);
+ AddLogicalGroupRules(BenchmarkLogicalGroupRule.ByParams);
+ }
+ }
+
+ private CSharpCompilation _compilation = null!;
+ private GeneratorDriver _driver = null!;
+
+ [GlobalSetup]
+ public async Task GlobalSetup()
+ {
+ var frameworkReferences = await ReferenceAssemblies.Net.Net100
+ .ResolveAsync(CSharpParseOptions.Default.Language, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ _compilation = CSharpCompilation.Create(nameof(LocalCompare))
+ .WithOptions(new(OutputKind.DynamicallyLinkedLibrary))
+ .AddReferences(frameworkReferences)
+ .AddReferences(MetadataReference
+ .CreateFromFile(typeof(AutoConstructAttribute).Assembly.Location))
+ .AddSyntaxTrees(Enumerable.Range(1, ServiceCount)
+ .Select(i => CSharpSyntaxTree.ParseText($"public interface IService{i:D4};")))
+ .AddSyntaxTrees(Enumerable.Range(1, TargetsCount).Select(TargetClassGenerator));
+
+ _driver = CSharpGeneratorDriver.Create(new AutoConstructSourceGenerator());
+ }
+
+ private SyntaxTree TargetClassGenerator(int i)
+ {
+ var rng = new Random(i);
+
+ var sb = new StringBuilder();
+
+ sb.AppendLine("[AutoCtor.AutoConstruct]");
+ sb.AppendLine($"public class Target{i:D4}");
+ sb.AppendLine("{");
+
+ var count = rng.Next(ServiceCount);
+ var items = rng.GetItems(Enumerable.Range(1, ServiceCount).ToArray(), count);
+ foreach (var item in items)
+ {
+ sb.Append($" private readonly IService{item:D4} _service{item:D4};");
+ }
+
+ sb.AppendLine("}");
+
+ return CSharpSyntaxTree.ParseText(sb.ToString());
+ }
+
+ [Benchmark]
+ public GeneratorDriver Standard() => _driver.RunGenerators(_compilation);
+}
diff --git a/src/CompileBenchmarks/Program.cs b/src/CompileBenchmarks/Program.cs
new file mode 100644
index 00000000..1e9f259f
--- /dev/null
+++ b/src/CompileBenchmarks/Program.cs
@@ -0,0 +1,12 @@
+using System.Reflection;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Order;
+using BenchmarkDotNet.Running;
+
+// Microsoft.CodeAnalysis.Analyzer.Testing ships non-optimized. It is only used to resolve
+// reference assemblies during setup, never on a measured path, so the validator is disabled.
+var config = ManualConfig.Create(DefaultConfig.Instance)
+ .WithOptions(ConfigOptions.DisableLogFile | ConfigOptions.DisableOptimizationsValidator)
+ .WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest));
+
+BenchmarkRunner.Run(Assembly.GetExecutingAssembly(), config);
diff --git a/src/CompileBenchmarks/Scanner.cs b/src/CompileBenchmarks/Scanner.cs
new file mode 100644
index 00000000..6d5c6239
--- /dev/null
+++ b/src/CompileBenchmarks/Scanner.cs
@@ -0,0 +1,44 @@
+using AutoCtor;
+using BenchmarkDotNet.Attributes;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Testing;
+
+namespace CompileBenchmarks;
+
+[ShortRunJob]
+public class Scanner
+{
+ [Params(500, 1000, 5000)]
+ public int PluginCount { get; set; }
+
+ private CSharpCompilation _compilation = null!;
+ private GeneratorDriver _driver = null!;
+
+ [GlobalSetup]
+ public async Task GlobalSetup()
+ {
+ var frameworkReferences = await ReferenceAssemblies.Net.Net100
+ .ResolveAsync(CSharpParseOptions.Default.Language, CancellationToken.None)
+ .ConfigureAwait(false);
+
+ _compilation = CSharpCompilation.Create(nameof(LocalCompare))
+ .WithOptions(new(OutputKind.DynamicallyLinkedLibrary))
+ .AddReferences(frameworkReferences)
+ .AddReferences(MetadataReference
+ .CreateFromFile(typeof(AutoConstructAttribute).Assembly.Location))
+ .AddSyntaxTrees(CSharpSyntaxTree.ParseText("public interface IPlugin;"))
+ .AddSyntaxTrees(Enumerable.Range(1, PluginCount)
+ .Select(i => CSharpSyntaxTree.ParseText($"public class Plugin{i:D4} : IPlugin;")))
+ .AddSyntaxTrees(CSharpSyntaxTree.ParseText(@"
+[AutoCtor.ServiceProvider]
+[AutoCtor.ScanSingleton(typeof(IPlugin), FromAssembliesOf = new[] { typeof(Container) })]
+public partial class Container;
+"));
+
+ _driver = CSharpGeneratorDriver.Create(new AutoConstructSourceGenerator());
+ }
+
+ [Benchmark]
+ public GeneratorDriver Scan() => _driver.RunGenerators(_compilation);
+}
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 851d0745..a016be45 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -5,13 +5,15 @@
+
-
+
+
@@ -19,4 +21,4 @@
-
\ No newline at end of file
+
diff --git a/src/Shared/AnalyzerReleases.Unshipped.md b/src/Shared/AnalyzerReleases.Unshipped.md
index 62bfdc21..ab365b59 100644
--- a/src/Shared/AnalyzerReleases.Unshipped.md
+++ b/src/Shared/AnalyzerReleases.Unshipped.md
@@ -1,2 +1,28 @@
-; Unshipped analyzer release
+; Unshipped analyzer release
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-------
+ACTR010 | AutoCtor | Error | ServiceImplementationMustHaveSinglePublicConstructor
+ACTR011 | AutoCtor | Error | ServiceImplementationCannotBeInstantiated
+ACTR012 | AutoCtor | Error | ServiceDependencyNotRegistered
+ACTR013 | AutoCtor | Error | CircularServiceDependency
+ACTR014 | AutoCtor | Error | KeyedServiceDependencyNotRegistered
+ACTR015 | AutoCtor | Error | ImplementationNotAssignableToService
+ACTR016 | AutoCtor | Error | InvalidOpenGenericRegistration
+ACTR017 | AutoCtor | Error | InvalidServiceProviderType
+ACTR018 | AutoCtor | Warning | TransientServiceCaptured
+ACTR019 | AutoCtor | Error | ScopedServiceCapturedBySingleton
+ACTR020 | AutoCtor | Error | OpenGenericConstraintNotSatisfied
+ACTR021 | AutoCtor | Error | InvalidServiceFactory
+ACTR022 | AutoCtor | Error | ServiceFactoryReturnTypeNotAssignable
+ACTR023 | AutoCtor | Error | OpenGenericServiceFactory
+ACTR024 | AutoCtor | Error | InvalidProviderFallback
+ACTR025 | AutoCtor | Error | ProviderFallbackNotAServiceProvider
+ACTR026 | AutoCtor | Warning | ScanFoundNoTypes
+ACTR027 | AutoCtor | Warning | ImportedModuleHasNoRegistrations
+ACTR028 | AutoCtor | Error | ModuleFactoryMustBeStatic
+ACTR029 | AutoCtor | Error | ServiceProviderMustBeSealed
+ACTR030 | AutoCtor | Error | ScanHasNoFilter
diff --git a/src/Shared/AttributeSourceGenerator/Emitter.cs b/src/Shared/AttributeSourceGenerator/Emitter.cs
index d021c88b..0e2aebcb 100644
--- a/src/Shared/AttributeSourceGenerator/Emitter.cs
+++ b/src/Shared/AttributeSourceGenerator/Emitter.cs
@@ -17,7 +17,7 @@ public static SourceText GenerateSource()
source.AppendLine("#if AUTOCTOR_EMBED_ATTRIBUTES");
using (source.StartBlock("namespace AutoCtor"))
{
- source.AddGeneratedAttributes(AttributeTargets.Enum);
+ source.AddGeneratedCodeAttribute();
using (source.StartBlock("internal enum GuardSetting"))
{
source.AppendLine("Default,");
@@ -25,37 +25,122 @@ public static SourceText GenerateSource()
source.AppendLine("Enabled");
}
- source.AddGeneratedAttributes(AttributeTargets.Class);
- source.AppendLine("[global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]");
- source.AppendLine("internal sealed class AutoConstructAttribute : global::System.Attribute");
- using (source.StartBlock())
+ source.AddGeneratedCodeAttribute();
+ EmitAttributeUsage(source, "Class", "Struct");
+ using (StartAttribute(source, "AutoConstructAttribute"))
{
- source.AppendLine("public AutoConstructAttribute(GuardSetting guard = GuardSetting.Default)");
- source.StartBlock().Dispose();
+ source.AppendLine("public AutoConstructAttribute(GuardSetting guard = GuardSetting.Default) { }");
}
- source.AddGeneratedAttributes(AttributeTargets.Class);
- source.AppendLine("[global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)]");
- source.AppendLine("internal sealed class AutoPostConstructAttribute : global::System.Attribute");
- source.StartBlock().Dispose();
+ source.AddGeneratedCodeAttribute();
+ EmitAttributeUsage(source, "Method");
+ StartAttribute(source, "AutoPostConstructAttribute").Dispose();
- source.AddGeneratedAttributes(AttributeTargets.Class);
- source.AppendLine("[global::System.AttributeUsage(global::System.AttributeTargets.Field | global::System.AttributeTargets.Property, AllowMultiple = false, Inherited = false)]");
- source.AppendLine("internal sealed class AutoConstructIgnoreAttribute : global::System.Attribute");
- source.StartBlock().Dispose();
+ source.AddGeneratedCodeAttribute();
+ EmitAttributeUsage(source, "Field", "Property");
+ StartAttribute(source, "AutoConstructIgnoreAttribute").Dispose();
- source.AddGeneratedAttributes(AttributeTargets.Class);
- source.AppendLine("[global::System.AttributeUsage(global::System.AttributeTargets.Field | global::System.AttributeTargets.Property | global::System.AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]");
- source.AppendLine("internal sealed class AutoKeyedServiceAttribute : global::System.Attribute");
- using (source.StartBlock())
+ source.AddGeneratedCodeAttribute();
+ EmitAttributeUsage(source, "Field", "Property", "Parameter");
+ using (StartAttribute(source, "AutoKeyedServiceAttribute"))
{
source.AppendLine("public object Key { get; }");
source.AppendLine("public AutoKeyedServiceAttribute(object key) => Key = key;");
}
+
+ source.AddGeneratedCodeAttribute();
+ EmitAttributeUsage(source, "Class");
+ using (StartAttribute(source, "ServiceProviderAttribute"))
+ {
+ source.AppendLine("public string Fallback { get; set; }");
+ }
+
+ source.AddGeneratedCodeAttribute();
+ using (source.StartBlock("internal abstract class ServiceAttribute : global::System.Attribute"))
+ {
+ source.AppendLine("public object Key { get; set; }");
+ source.AppendLine("public string Factory { get; set; }");
+ }
+
+ string[] lifetimes = ["Singleton", "Transient", "Scoped"];
+
+ foreach (var lifetime in lifetimes)
+ {
+ source.AddGeneratedCodeAttribute();
+ EmitAttributeUsage(source, true, false, "Class");
+ using (StartAttribute(source, lifetime + "Attribute", "ServiceAttribute"))
+ {
+ source.AppendLine($"public {lifetime}Attribute(global::System.Type service, global::System.Type implementation = null) {{ }}");
+ }
+ }
+
+ source.AddGeneratedCodeAttribute();
+ EmitAttributeUsage(source, true, false, "Class");
+ using (StartAttribute(source, "ImportAttribute"))
+ {
+ source.AppendLine("public ImportAttribute(global::System.Type module) { }");
+ }
+
+ source.AddGeneratedCodeAttribute();
+ source.AppendLine("[global::System.Flags]");
+ using (source.StartBlock("internal enum ScanAs"))
+ {
+ source.AppendLine("Service = 1,");
+ source.AppendLine("Self = 2,");
+ source.AppendLine("ImplementedInterfaces = 4");
+ }
+
+ using (source.StartBlock("internal abstract class ScanAttribute : global::System.Attribute"))
+ {
+ source.AppendLine("public ScanAs As { get; set; }");
+ source.AppendLine("public global::System.Type[] FromAssembliesOf { get; set; }");
+ source.AppendLine("public string TypeNameFilter { get; set; }");
+ }
+
+ foreach (var lifetime in lifetimes)
+ {
+ source.AddGeneratedCodeAttribute();
+ EmitAttributeUsage(source, true, false, "Class");
+ using (StartAttribute(source, $"Scan{lifetime}Attribute", "ScanAttribute"))
+ {
+ source.AppendLine($"public Scan{lifetime}Attribute(global::System.Type service = null) {{ }}");
+ }
+ }
+
+ source.AppendLine("#if AUTOCTOR_EMBED_GENERIC_ATTRIBUTES");
+ foreach (var lifetime in lifetimes)
+ {
+ source.AddGeneratedCodeAttribute();
+ EmitAttributeUsage(source, true, false, "Class");
+ source.AppendLine($"internal sealed class {lifetime}Attribute : ServiceAttribute {{ }}");
+ source.AddGeneratedCodeAttribute();
+ EmitAttributeUsage(source, true, false, "Class");
+ source.AppendLine($"internal sealed class {lifetime}Attribute : ServiceAttribute {{ }}");
+ }
+ source.AddGeneratedCodeAttribute();
+ EmitAttributeUsage(source, true, false, "Class");
+ source.AppendLine("internal sealed class ImportAttribute : global::System.Attribute { }");
+ source.AppendLine("#endif");
}
source.AppendLine("#endif");
return source;
}
+
+ private static void EmitAttributeUsage(CodeBuilder source, params string[] targets)
+ => EmitAttributeUsage(source, false, false, targets);
+
+ private static void EmitAttributeUsage(CodeBuilder source, bool allowMultiple, bool inherited, params string[] targets)
+ {
+ var targetString = string.Join(" | ", targets.Select(t => $"global::System.AttributeTargets.{t}"));
+
+ source.AppendLine($"[global::System.AttributeUsage({targetString}, AllowMultiple = {allowMultiple}, Inherited = {inherited})]");
+ }
+
+ private static IDisposable StartAttribute(CodeBuilder source, string typeName, string baseType = "global::System.Attribute")
+ {
+ source.AppendLine($"internal sealed class {typeName} : {baseType}");
+ return source.StartBlock();
+ }
}
}
diff --git a/src/Shared/AutoConstructSourceGenerator/Emitter.cs b/src/Shared/AutoConstructSourceGenerator/Emitter.cs
index 42f68bff..508fa605 100644
--- a/src/Shared/AutoConstructSourceGenerator/Emitter.cs
+++ b/src/Shared/AutoConstructSourceGenerator/Emitter.cs
@@ -18,13 +18,26 @@ private static class Emitter
{
public static void GenerateSource(
EmitterContext context,
- ((ImmutableArray Types, ImmutableArray PostCtorMethods) Models,
- bool Guards) input)
+ (ImmutableArray Types,
+ ImmutableArray PostCtorMethods,
+ bool Guards,
+ ImmutableArray Providers,
+ DuckTypes DuckTypes) input)
{
- if (input.Models.Types.IsDefaultOrEmpty) return;
+ if (input.Types.IsDefaultOrEmpty && input.Providers.IsDefaultOrEmpty) return;
var ctorMaps = new Dictionary();
- var orderedTypes = input.Models.Types.OrderBy(static t => t.Depth);
+ var orderedTypes = input.Types.OrderBy(static t => t.Depth);
+
+ // Indexed once rather than scanned per type: a marked method on every
+ // [AutoConstruct] type in a large solution makes that scan quadratic.
+ var postCtorsByType = new Dictionary>();
+ foreach (var method in input.PostCtorMethods)
+ {
+ if (!postCtorsByType.TryGetValue(method.TypeKey, out var marked))
+ postCtorsByType.Add(method.TypeKey, marked = []);
+ marked.Add(method);
+ }
foreach (var type in orderedTypes)
{
@@ -67,9 +80,7 @@ public static void GenerateSource(
}
}
- var postCtorMethods = input.Models.PostCtorMethods
- .Where(m => m.TypeKey == type.TypeKey)
- .ToImmutableArray();
+ postCtorsByType.TryGetValue(type.TypeKey, out var postCtorMethods);
var (source, parameters) = GenerateSource(context, type, postCtorMethods, baseParameters, input.Guards);
@@ -80,12 +91,16 @@ public static void GenerateSource(
context.AddSource($"{type.HintName}.g.cs", source);
}
+
+ // After the loop above, as ctorMaps holds the constructors AutoCtor generates.
+ foreach (var provider in input.Providers)
+ IoCEmitter.Generate(context, provider, ctorMaps, input.DuckTypes);
}
private static (SourceText?, ParameterList?) GenerateSource(
EmitterContext context,
TypeModel type,
- ImmutableArray markedPostCtorMethods,
+ List? markedPostCtorMethods,
IEnumerable? baseParameters,
bool guards)
{
@@ -118,14 +133,13 @@ private static (SourceText?, ParameterList?) GenerateSource(
using (source.StartPartialType(type))
{
- source
- .AddGeneratedAttributes(AttributeTargets.Method);
+ source.AddGeneratedCodeAttribute();
source.AppendIndent()
- .Append($"public {type.Name}({parameters.CtorParameterDeclarations:commaindent})")
- .Append(parameters.HasBaseParameters,
- $" : base({parameters.BaseParameters:commaindent})")
- .AppendLine();
+ .Append($"public {type.Name}({parameters.CtorParameterDeclarations:commaindent})");
+ if (parameters.HasBaseParameters)
+ source.Append($" : base({parameters.BaseParameters:commaindent})");
+ source.AppendLine();
using (source.StartBlock())
{
@@ -142,10 +156,10 @@ private static (SourceText?, ParameterList?) GenerateSource(
&& !item.IsNullableAnnotated;
source.AppendIndent()
- .Append($"{item.IdentifierName} = {parameter}")
- .Append(addGuard,
- $" ?? throw new global::System.ArgumentNullException(\"{parameter}\")")
- .Append(";")
+ .Append($"{item.IdentifierName} = {parameter}");
+ if (addGuard)
+ source.Append($" ?? throw new global::System.ArgumentNullException(\"{parameter}\")");
+ source.Append(";")
.AppendLine();
}
if (postCtorMethod.HasValue)
@@ -176,7 +190,7 @@ private static ITypeSymbol FindTypeForArgument(
return type;
}
- private static ITypeSymbol SetGenerics(
+ internal static ITypeSymbol SetGenerics(
ITypeSymbol type,
EquatableList parameters,
EquatableList arguments)
@@ -204,10 +218,13 @@ private static ITypeSymbol SetGenerics(
private static PostCtorModel? GetPostCtorMethod(
EmitterContext context,
TypeModel type,
- ImmutableArray markedPostCtorMethods)
+ List? markedPostCtorMethods)
{
+ if (markedPostCtorMethods is null)
+ return null;
+
// ACTR001
- if (markedPostCtorMethods.Length > 1)
+ if (markedPostCtorMethods.Count > 1)
{
foreach (var m in markedPostCtorMethods)
{
@@ -216,7 +233,7 @@ private static ITypeSymbol SetGenerics(
return null;
}
- if (markedPostCtorMethods.Length != 1)
+ if (markedPostCtorMethods.Count != 1)
return null;
var method = markedPostCtorMethods[0];
diff --git a/src/Shared/AutoConstructSourceGenerator/IoCEmitter.cs b/src/Shared/AutoConstructSourceGenerator/IoCEmitter.cs
new file mode 100644
index 00000000..9beaeaa5
--- /dev/null
+++ b/src/Shared/AutoConstructSourceGenerator/IoCEmitter.cs
@@ -0,0 +1,542 @@
+using Microsoft.CodeAnalysis;
+using static AutoCtor.Diagnostics;
+
+#if ROSLYN_3
+using EmitterContext = Microsoft.CodeAnalysis.GeneratorExecutionContext;
+#elif ROSLYN_4
+using EmitterContext = Microsoft.CodeAnalysis.SourceProductionContext;
+#endif
+
+namespace AutoCtor;
+
+public partial class AutoConstructSourceGenerator
+{
+ private static class IoCEmitter
+ {
+ private const string DI = "global::Microsoft.Extensions.DependencyInjection.";
+
+ private sealed class Node(int id, int order, RegistrationModel registration, ITypeSymbol service, INamedTypeSymbol implementation)
+ {
+ public string Name { get; } = $"S{id}";
+ // Registration order, which decides the winner of a single resolve.
+ public int Order { get; } = order;
+ public RegistrationModel R { get; } = registration;
+ public string Service { get; } = service.ToDisplayString(FullyQualifiedFormat);
+ public INamedTypeSymbol Implementation { get; } = implementation;
+ public List Dependencies { get; } = [];
+ public string Create { get; set; } = "";
+ public bool Scoped { get; set; } = registration.Lifetime == Lifetime.Scoped;
+ public int VisitState { get; set; }
+ }
+
+ public static void Generate(
+ EmitterContext context,
+ ServiceProviderModel provider,
+ Dictionary ctorMaps,
+ DuckTypes duck)
+ {
+ if (ReportDiagnostics(context, provider.Diagnostics))
+ return;
+
+ if (provider.Registrations.Count == 0)
+ return;
+
+ List builtIns = ["global::System.IServiceProvider"];
+ if (duck.DI)
+ {
+ builtIns.Add(DI + "IServiceScopeFactory");
+ builtIns.Add(DI + "IServiceProviderIsService");
+ }
+ if (duck.Keyed)
+ {
+ builtIns.Add(DI + "IKeyedServiceProvider");
+ builtIns.Add(DI + "IServiceProviderIsKeyedService");
+ }
+
+ var nodes = CreateNodes(context, provider, ctorMaps, duck, builtIns, out var failed);
+
+ foreach (var node in nodes)
+ failed |= !VerifyNodes(context, node);
+
+ if (failed)
+ return;
+
+ // Registrations first, so they win over collections and built ins of the same type.
+ var singles = new List<(string Type, string Key, string Value, bool Scoped)>();
+ var collections = new List<(string Type, string Key, string Value, bool Scoped)>();
+
+ foreach (var group in nodes.GroupBy(n => (n.Service, n.R.Key)))
+ {
+ var ordered = group.OrderBy(n => n.Order).ToList();
+ var last = ordered[ordered.Count - 1];
+ singles.Add((group.Key.Service, group.Key.Key ?? "null", $"{last.Name}()", last.Scoped));
+ collections.Add(($"global::System.Collections.Generic.IEnumerable<{group.Key.Service}>", group.Key.Key ?? "null",
+ NewArray(group.Key.Service, ordered), ordered.Any(n => n.Scoped)));
+ }
+
+ if (duck.Keyed)
+ {
+ // KeyedService.AnyKey asks for every keyed registration of a service.
+ foreach (var group in nodes.Where(n => n.R.Key is not null).GroupBy(n => n.Service))
+ {
+ var ordered = group.OrderBy(n => n.Order).ToList();
+ collections.Add(($"global::System.Collections.Generic.IEnumerable<{group.Key}>", DI + "KeyedService.AnyKey",
+ NewArray(group.Key, ordered), ordered.Any(n => n.Scoped)));
+ }
+ }
+
+ var entries = singles.Concat(collections).Concat(builtIns.Select(b => (b, "null", "this", false))).ToList();
+
+ var source = new CodeBuilder()
+ .AppendHeader()
+ .AppendLine()
+ .AppendLine("#nullable enable")
+ .AppendLine();
+
+ List baseTypes = [
+ .. builtIns,
+ "global::System.IDisposable"
+ ];
+ if (duck.Async)
+ baseTypes.Add("global::System.IAsyncDisposable");
+
+ using (source.StartPartialType(provider, [
+ .. baseTypes,
+ .. Resolvers(entries, false).Select(t => $"{provider.Name}.IResolver<{t}>")]))
+ {
+ if (duck.DI)
+ baseTypes.Add(DI + "IServiceScope");
+
+ EmitContainer(source, provider, nodes, entries, duck, inScope: false);
+ source.AppendLine();
+ source.AddGeneratedCodeAttribute();
+ using (source.StartType("public sealed class Scope", [
+ .. baseTypes,
+ .. Resolvers(entries, true).Select(t => $"{provider.Name}.IResolver<{t}>")]))
+ EmitContainer(source, provider, nodes, entries, duck, inScope: true);
+ }
+
+ context.AddSource($"{provider.HintName}.ServiceProvider.g.cs", source);
+ }
+
+ private static bool ReportDiagnostics(EmitterContext context, IEnumerable diagnostics)
+ {
+ var hasError = false;
+ foreach (var diagnostic in diagnostics)
+ {
+ context.ReportDiagnostic(diagnostic, diagnostic.Descriptor, [.. diagnostic.Args]);
+ hasError = hasError || diagnostic.Descriptor.DefaultSeverity == DiagnosticSeverity.Error;
+ }
+ return hasError;
+ }
+
+ // The first registration of each unkeyed type wins, the same as in Resolve.
+ private static IEnumerable Resolvers(List<(string Type, string Key, string Value, bool Scoped)> entries, bool inScope)
+ => entries.Where(e => e.Key == "null" && (inScope || !e.Scoped)).Select(e => e.Type).Distinct();
+
+ // Every registration of a service, closing any open generic rule that matches it.
+ // A rule whose constraints the type arguments break is skipped, and named in unsatisfied.
+ private static List Find(
+ ServiceProviderModel provider,
+ List nodes,
+ ITypeSymbol type,
+ string? key,
+ out RegistrationModel? unsatisfied)
+ {
+ var registrations = provider.Registrations;
+ unsatisfied = null;
+ var name = type.ToDisplayString(FullyQualifiedFormat);
+ if (type is INamedTypeSymbol { IsGenericType: true } closed)
+ {
+ var definition = closed.OriginalDefinition.ToDisplayString(FullyQualifiedFormat);
+ for (var i = 0; i < registrations.Count; i++)
+ {
+ var r = registrations[i];
+ if (r.IsOpenGeneric && r.Key == key
+ && r.Service.ToString() == definition
+ && r.Implementation.TypeSymbol is INamedTypeSymbol open
+ && open.Arity == closed.Arity
+ && !nodes.Any(n => n.Order == i && n.Service == name))
+ {
+ if (!SatisfiesConstraints(open, closed.TypeArguments))
+ {
+ unsatisfied = r;
+ continue;
+ }
+ nodes.Add(new(nodes.Count, i, r, closed, open.Construct([.. closed.TypeArguments])));
+ }
+ }
+ }
+ return nodes.Where(n => n.Service == name && n.R.Key == key).OrderBy(n => n.Order).ToList();
+ }
+
+ private static List CreateNodes(
+ EmitterContext context,
+ ServiceProviderModel provider,
+ Dictionary ctorMaps,
+ DuckTypes duck,
+ List builtIns,
+ out bool failed)
+ {
+ failed = false;
+
+ var nodes = new List();
+
+ for (var i = 0; i < provider.Registrations.Count; i++)
+ {
+ var r = provider.Registrations[i];
+ if (!r.IsOpenGeneric)
+ nodes.Add(new(
+ id: nodes.Count,
+ order: i,
+ registration: r,
+ service: r.Service.TypeSymbol,
+ implementation: (INamedTypeSymbol)r.Implementation.TypeSymbol));
+ }
+
+ // Closing an open generic adds a node, so this walks the list as it grows.
+ for (var i = 0; i < nodes.Count; i++)
+ {
+ var node = nodes[i];
+ IEnumerable parameters = node.R.Parameters;
+ if (node.R.Factory is null)
+ {
+ if (node.R.IsAutoConstruct
+ && ctorMaps.TryGetValue(TypeModel.CreateKey(node.Implementation), out var predicted))
+ {
+ // The constructor AutoCtor is about to generate.
+ parameters = predicted.Distinct();
+ }
+ else if (node.R.PublicConstructorCount != 1)
+ {
+ context.ReportDiagnostic(node.R,
+ ACTR010_ServiceImplementationMustHaveSinglePublicConstructor);
+ failed = true;
+ continue;
+ }
+ }
+
+ var typeParameters = node.Implementation.OriginalDefinition.TypeParameters
+ .Select(ConvertToEquatable)
+ .ToEquatableList();
+ var typeArguments = node.Implementation.TypeArguments
+ .Select(ConvertToEquatable)
+ .ToEquatableList();
+ var argList = parameters
+ .Select(p => Argument(
+ context,
+ provider,
+ duck,
+ builtIns,
+ nodes,
+ node,
+ type: Emitter.SetGenerics(p.Type.TypeSymbol, typeParameters, typeArguments),
+ p.KeyedService,
+ p.ErrorName))
+ .ToList();
+
+ if (argList.Contains(null))
+ failed = true;
+
+ var args = string.Join(", ", argList);
+
+ node.Create = node.R.Factory is { } factory
+ ? node.R.FactoryIsMethod ? $"{factory}({args})" : factory
+ : $"new {node.Implementation.ToDisplayString(FullyQualifiedFormat)}({args})";
+ }
+
+ return nodes;
+ }
+
+ // The expression that resolves one constructor argument, or null after reporting why it can't.
+ private static string? Argument(
+ EmitterContext context,
+ ServiceProviderModel provider,
+ DuckTypes duck,
+ List builtIns,
+ List nodes,
+ Node node,
+ ITypeSymbol type,
+ string? key,
+ string parameterName)
+ {
+ var name = type.ToDisplayString(FullyQualifiedFormat);
+ var typeName = type.ToDisplayString(MinimallyQualifiedFormat);
+ var found = Find(provider, nodes, type, key, out var unsatisfied);
+ if (found.Count > 0)
+ {
+ node.Dependencies.Add(found[found.Count - 1]);
+ return $"{found[found.Count - 1].Name}()";
+ }
+ if (unsatisfied is { } rule)
+ {
+ context.ReportDiagnostic(rule, ACTR020_OpenGenericConstraintNotSatisfied, typeName, parameterName, rule.ErrorName);
+ return null;
+ }
+
+ if (type is INamedTypeSymbol { ConstructedFrom.SpecialType: SpecialType.System_Collections_Generic_IEnumerable_T } enumerable)
+ {
+ var elements = Find(provider, nodes, enumerable.TypeArguments[0], key, out _);
+ node.Dependencies.AddRange(elements);
+ return NewArray(enumerable.TypeArguments[0].ToDisplayString(FullyQualifiedFormat), elements);
+ }
+
+ if (key is null && builtIns.Contains(name))
+ return "this";
+
+ if (provider.Fallback is not null && (key is null || duck.Keyed))
+ return $"Required<{name}>(GetKeyedService(typeof({name}), {key ?? "null"}))";
+
+ if (key is null)
+ context.ReportDiagnostic(node.R, ACTR012_ServiceDependencyNotRegistered, typeName, node.R.ErrorName);
+ else
+ context.ReportDiagnostic(node.R, ACTR014_KeyedServiceDependencyNotRegistered, typeName, node.R.ErrorName, key);
+ return null;
+ }
+
+ // Cycles, and which services can only live in a scope. False when an error was reported.
+ private static bool VerifyNodes(EmitterContext context, Node node)
+ {
+ if (node.VisitState == 2)
+ return true;
+ if (node.VisitState == 1)
+ {
+ context.ReportDiagnostic(node.R, ACTR013_CircularServiceDependency);
+ return false;
+ }
+
+ var ok = true;
+ node.VisitState = 1;
+ foreach (var dependency in node.Dependencies)
+ {
+ ok &= VerifyNodes(context, dependency);
+ node.Scoped |= dependency.Scoped;
+ if (node.R.Lifetime != Lifetime.Singleton)
+ continue;
+
+ if (dependency.Scoped)
+ {
+ context.ReportDiagnostic(node.R, ACTR019_ScopedServiceCapturedBySingleton, dependency.R.ErrorName, node.R.ErrorName);
+ ok = false;
+ }
+ else if (dependency.R.Lifetime == Lifetime.Transient)
+ {
+ context.ReportDiagnostic(node.R, ACTR018_TransientServiceCaptured, dependency.R.ErrorName, node.R.ErrorName);
+ }
+ }
+ node.VisitState = 2;
+ return ok;
+ }
+
+ private static bool SatisfiesConstraints(INamedTypeSymbol definition, IReadOnlyList arguments)
+ {
+ var typeParameters = definition.TypeParameters
+ .Select(ConvertToEquatable)
+ .ToEquatableList();
+ var typeArguments = arguments
+ .Select(ConvertToEquatable)
+ .ToEquatableList();
+
+ return definition.TypeParameters.Zip(arguments, (p, a) =>
+ (!p.HasReferenceTypeConstraint || a.IsReferenceType)
+ && (!p.HasValueTypeConstraint || a.IsValueType)
+ && (!p.HasUnmanagedTypeConstraint || a.IsUnmanagedType)
+ && (!p.HasConstructorConstraint || a.IsValueType
+ || a is INamedTypeSymbol { IsAbstract: false } named && named.InstanceConstructors.Any(c => c.Parameters.Length == 0 && c.DeclaredAccessibility == Accessibility.Public))
+ && p.ConstraintTypes.All(c => ServiceProviderModel.IsAssignable(a, Emitter.SetGenerics(c, typeParameters, typeArguments))))
+ .All(ok => ok);
+ }
+
+ private static string NewArray(string elementType, List elements)
+ => $"new {elementType}[] {{ {string.Join(", ", elements.Select(static n => $"{n.Name}()"))} }}";
+
+ private static void EmitContainer(
+ CodeBuilder source,
+ ServiceProviderModel provider,
+ List nodes,
+ List<(string Type, string Key, string Value, bool Scoped)> entries,
+ DuckTypes duck,
+ bool inScope)
+ {
+ var root = inScope ? "_root" : "this";
+
+ source.AppendLine("private readonly object _lock = new object();");
+ source.AppendLine("private global::System.Collections.Generic.List? _disposables;");
+ if (inScope)
+ {
+ source.AppendLine($"private readonly {provider.Name} _root;");
+ source.AppendLine($"public Scope({provider.Name} root) => _root = root;");
+ source.AppendLine("public global::System.IServiceProvider ServiceProvider => this;");
+ }
+ source.AppendLine();
+
+ foreach (var node in nodes)
+ {
+ if (node.Scoped && !inScope)
+ continue;
+
+ var (name, type, create) = (node.Name, node.Service, node.Create.Replace("{root}", root));
+ var tracked = node.R.ImplementationIsDisposable ? $"Track<{type}>({create})" : create;
+ if (inScope && node.R.Lifetime == Lifetime.Singleton)
+ {
+ source.AppendLine($"private {type} {name}() => _root.{name}();");
+ }
+ else if (node.R.Lifetime == Lifetime.Transient)
+ {
+ source.AppendLine($"private {type} {name}() => {tracked};");
+ }
+ else
+ {
+ // Only the first resolve takes the lock. volatile needs a reference type, so a
+ // value type is held boxed.
+ var (field, cast) = node.R.ServiceIsReferenceType
+ ? ($"{type}?", "")
+ : ("object?", $"({type})");
+ source.AppendLine($"private volatile {field} _{name};");
+ using (source.StartBlock($"private {type} {name}()"))
+ {
+ source.AppendLine($"var service = _{name};");
+ source.AppendLine("if (service is not null)")
+ .IncreaseIndent()
+ .AppendLine($"return {cast}service;")
+ .DecreaseIndent();
+ source.AppendLine("lock (_lock)")
+ .IncreaseIndent()
+ .AppendLine($"return {cast}(_{name} ??= {tracked});")
+ .DecreaseIndent();
+ }
+ }
+ }
+ source.AppendLine();
+
+ var visible = entries.Where(e => inScope || !e.Scoped).ToList();
+ using (source.StartBlock("private object? Resolve(global::System.Type type, object? key, bool probe)"))
+ {
+ using (source.StartBlock("if (key is null)"))
+ {
+ foreach (var (type, _, value, _) in visible.Where(e => e.Key == "null"))
+ source.AppendLine($"if (type == typeof({type}))")
+ .IncreaseIndent()
+ .AppendLine($"return probe ? this : (object)({value});")
+ .DecreaseIndent();
+ source.AppendLine("return null;");
+ }
+ foreach (var (type, key, value, _) in visible.Where(e => e.Key != "null"))
+ source.AppendLine($"if (type == typeof({type}) && object.Equals(key, {key}))")
+ .IncreaseIndent()
+ .AppendLine($"return probe ? this : (object)({value});")
+ .DecreaseIndent();
+ source.AppendLine("return null;");
+ }
+ source.AppendLine();
+
+ // A scope gets its own scope of the fallback, so the fallback's scoped services work.
+ var fallback = provider.Fallback?.Replace("{root}", root) ?? "null";
+ if (inScope && duck.DI && provider.Fallback is not null)
+ {
+ source.AppendLine("private global::System.IServiceProvider? _fallback;");
+ source.AppendLine($"private global::System.IServiceProvider? Fallback {{ get {{ lock (_lock) return _fallback ??= {fallback} is {DI}IServiceScopeFactory f ? Track(f.CreateScope()).ServiceProvider : {fallback}; }} }}");
+ }
+ else
+ {
+ source.AppendLine($"private global::System.IServiceProvider? Fallback => {fallback};");
+ }
+
+ var keyedFallback = duck.Keyed ? $"(Fallback as {DI}IKeyedServiceProvider)?.GetKeyedService(serviceType, serviceKey)" : "null";
+ var fallbackProbe = !duck.DI ? ""
+ : $" || (serviceKey == null ? Fallback is {DI}IServiceProviderIsService s && s.IsService(serviceType) : "
+ + (duck.Keyed ? $"Fallback is {DI}IServiceProviderIsKeyedService k && k.IsKeyedService(serviceType, serviceKey))" : "false)");
+
+ source.AppendLine()
+ .AppendLine("public object? GetService(global::System.Type serviceType)")
+ .AppendLine("\t=> GetKeyedService(serviceType, null);");
+
+ source.AppendLine()
+ .AppendLine("public object? GetKeyedService(global::System.Type serviceType, object? serviceKey)")
+ .AppendLine("\t=> Resolve(serviceType, serviceKey, false)")
+ .AppendLine($"\t?? (serviceKey == null ? Fallback?.GetService(serviceType) : {keyedFallback});");
+
+ source.AppendLine()
+ .AppendLine("public object GetRequiredKeyedService(global::System.Type serviceType, object? serviceKey)")
+ .AppendLine("\t=> GetKeyedService(serviceType, serviceKey)")
+ .AppendLine("\t?? throw new global::System.InvalidOperationException(\"No service for type '\" + serviceType + \"' has been registered.\");");
+
+ // Typed lookups: an interface check instead of a chain of type comparisons.
+ source.AppendLine()
+ .AppendLine("public T? GetService()")
+ .AppendLine($"\t=> this is {provider.Name}.IResolver resolver ? resolver.Get() : GetService(typeof(T)) is T service ? service : default;");
+
+ source.AppendLine()
+ .AppendLine("public T GetRequiredService()")
+ .AppendLine($"\t=> this is {provider.Name}.IResolver resolver ? resolver.Get() : (T)GetRequiredKeyedService(typeof(T), null);");
+
+ source.AppendLine();
+ if (!inScope)
+ source.AppendLine("private interface IResolver { T Get(); }");
+ foreach (var type in Resolvers(entries, inScope))
+ source
+ .AppendLine($"{type} {provider.Name}.IResolver<{type}>.Get()")
+ .AppendLine($"\t=> {visible.First(e => e.Type == type && e.Key == "null").Value};");
+
+ source.AppendLine()
+ .AppendLine("public bool IsService(global::System.Type serviceType)")
+ .AppendLine("\t=> IsKeyedService(serviceType, null);");
+
+ source.AppendLine()
+ .AppendLine($"public bool IsKeyedService(global::System.Type serviceType, object? serviceKey)")
+ .AppendLine($"\t=> Resolve(serviceType, serviceKey, true) != null{fallbackProbe};");
+
+ source.AppendLine();
+ source.AppendLine($"public Scope CreateScope() => new Scope({root});");
+ if (duck.DI)
+ source.AppendLine($"{DI}IServiceScope {DI}IServiceScopeFactory.CreateScope() => CreateScope();");
+
+ source.AppendLine()
+ .AppendLine("private static T Required(object? service)")
+ .AppendLine("\t=> service is T t ? t : throw new global::System.InvalidOperationException(\"No service for type '\" + typeof(T) + \"' has been registered.\");");
+
+ source.AppendLine();
+ using (source.StartBlock("private T Track(T service)"))
+ {
+ source.AppendLine($"if (service is global::System.IDisposable{(duck.Async ? " || service is global::System.IAsyncDisposable" : "")})");
+ source.AppendLine("\tlock (_lock)");
+ source.AppendLine("\t\t(_disposables ??= new global::System.Collections.Generic.List()).Add(service);");
+ source.AppendLine("return service;");
+ }
+
+ // Disposed in reverse order of creation, and only once.
+ source.AppendLine();
+ using (source.StartBlock("private object[] Drain()"))
+ using (source.StartBlock("lock (_lock)"))
+ {
+ source.AppendLine("var items = _disposables?.ToArray() ?? new object[0];");
+ source.AppendLine("_disposables = null;");
+ source.AppendLine("global::System.Array.Reverse(items);");
+ source.AppendLine("return items;");
+ }
+
+ source.AppendLine();
+ using (source.StartBlock("public void Dispose()"))
+ using (source.StartBlock("foreach (var item in Drain())"))
+ using (source.StartBlock("if (item is global::System.IDisposable disposable)"))
+ {
+ source.AppendLine("disposable.Dispose();");
+ }
+
+ if (duck.Async)
+ {
+ source.AppendLine();
+ using (source.StartBlock("public async global::System.Threading.Tasks.ValueTask DisposeAsync()"))
+ using (source.StartBlock("foreach (var item in Drain())"))
+ {
+ source.AppendLine("if (item is global::System.IAsyncDisposable d)");
+ source.AppendLine("\tawait d.DisposeAsync().ConfigureAwait(false);");
+ source.AppendLine("else");
+ source.AppendLine("\t((global::System.IDisposable)item).Dispose();");
+ }
+ }
+ }
+
+ private static EquatableTypeSymbol ConvertToEquatable(ITypeSymbol typeSymbol) => new(typeSymbol);
+ }
+}
diff --git a/src/Shared/AutoConstructSourceGenerator/Tracking.cs b/src/Shared/AutoConstructSourceGenerator/Tracking.cs
index 554f1e21..4dc6f3ea 100644
--- a/src/Shared/AutoConstructSourceGenerator/Tracking.cs
+++ b/src/Shared/AutoConstructSourceGenerator/Tracking.cs
@@ -10,11 +10,13 @@ public static class TrackingNames
public static string BuildProperties => nameof(BuildProperties);
public static string TypeModels => nameof(TypeModels);
public static string PostCtorMethods => nameof(PostCtorMethods);
+ public static string ServiceProviders => nameof(ServiceProviders);
public static IReadOnlyCollection AllTrackers { get; } = [
BuildProperties,
TypeModels,
PostCtorMethods,
+ ServiceProviders,
];
}
}
diff --git a/src/Shared/Constants/AttributeNames.cs b/src/Shared/Constants/AttributeNames.cs
index b7aef2c5..5de3c6d4 100644
--- a/src/Shared/Constants/AttributeNames.cs
+++ b/src/Shared/Constants/AttributeNames.cs
@@ -4,4 +4,5 @@
public const string AutoPostConstruct = "AutoCtor.AutoPostConstructAttribute";
public const string AutoConstructIgnore = "AutoCtor.AutoConstructIgnoreAttribute";
public const string AutoKeyedService = "AutoCtor.AutoKeyedServiceAttribute";
+ public const string ServiceProvider = "AutoCtor.ServiceProviderAttribute";
}
diff --git a/src/Shared/Constants/Diagnostics.cs b/src/Shared/Constants/Diagnostics.cs
index 73324787..24bf9864 100644
--- a/src/Shared/Constants/Diagnostics.cs
+++ b/src/Shared/Constants/Diagnostics.cs
@@ -112,4 +112,256 @@ internal static class Diagnostics
category: "AutoCtor",
DiagnosticSeverity.Warning,
isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR010
+ /// Title: Service implementation must have a single public constructor
+ ///
+ public static readonly DiagnosticDescriptor ACTR010_ServiceImplementationMustHaveSinglePublicConstructor = new DiagnosticDescriptor(
+ id: "ACTR010",
+ title: "Service implementation must have a single public constructor",
+ messageFormat: "The implementation type '{0}' must have exactly one public constructor to be used as a service",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR012
+ /// Title: Service dependency is not registered
+ ///
+ public static readonly DiagnosticDescriptor ACTR012_ServiceDependencyNotRegistered = new DiagnosticDescriptor(
+ id: "ACTR012",
+ title: "Service dependency is not registered",
+ messageFormat: "No registration was found for the dependency '{0}' of '{1}'",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR013
+ /// Title: Circular dependency between registered services
+ ///
+ public static readonly DiagnosticDescriptor ACTR013_CircularServiceDependency = new DiagnosticDescriptor(
+ id: "ACTR013",
+ title: "Circular dependency between registered services",
+ messageFormat: "A circular dependency was detected involving '{0}'",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR014
+ /// Title: Keyed service dependency is not registered
+ ///
+ public static readonly DiagnosticDescriptor ACTR014_KeyedServiceDependencyNotRegistered = new DiagnosticDescriptor(
+ id: "ACTR014",
+ title: "Keyed service dependency is not registered",
+ messageFormat: "No registration with key {2} was found for the dependency '{0}' of '{1}'",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR018
+ /// Title: Transient service is captured by a singleton
+ ///
+ public static readonly DiagnosticDescriptor ACTR018_TransientServiceCaptured = new DiagnosticDescriptor(
+ id: "ACTR018",
+ title: "Transient service is captured by a singleton",
+ messageFormat: "The transient service '{0}' is captured by the singleton '{1}' and will not be recreated for each resolve",
+ category: "AutoCtor",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR019
+ /// Title: Scoped service cannot be resolved outside a scope
+ ///
+ public static readonly DiagnosticDescriptor ACTR019_ScopedServiceCapturedBySingleton = new DiagnosticDescriptor(
+ id: "ACTR019",
+ title: "Scoped service cannot be resolved outside a scope",
+ messageFormat: "The service '{0}' requires a scope and cannot be injected into the singleton '{1}'",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR026
+ /// Title: Service scan found no types
+ ///
+ public static readonly DiagnosticDescriptor ACTR026_ScanFoundNoTypes = new DiagnosticDescriptor(
+ id: "ACTR026",
+ title: "Service scan found no types",
+ messageFormat: "The scan for '{0}' found no accessible, concrete, non-generic class to register",
+ category: "AutoCtor",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR027
+ /// Title: Imported module has no registrations
+ ///
+ public static readonly DiagnosticDescriptor ACTR027_ImportedModuleHasNoRegistrations = new DiagnosticDescriptor(
+ id: "ACTR027",
+ title: "Imported module has no registrations",
+ messageFormat: "The module '{0}' has no registrations; a module in another assembly needs that assembly to define AUTOCTOR_USAGES",
+ category: "AutoCtor",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR011
+ /// Title: Service implementation cannot be instantiated
+ ///
+ public static readonly DiagnosticDescriptor ACTR011_ServiceImplementationCannotBeInstantiated = new DiagnosticDescriptor(
+ id: "ACTR011",
+ title: "Service implementation cannot be instantiated",
+ messageFormat: "The implementation type '{0}' cannot be instantiated; it must be a non-abstract, non-generic class",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR015
+ /// Title: Implementation type is not assignable to the service type
+ ///
+ public static readonly DiagnosticDescriptor ACTR015_ImplementationNotAssignableToService = new DiagnosticDescriptor(
+ id: "ACTR015",
+ title: "Implementation type is not assignable to the service type",
+ messageFormat: "The implementation type '{0}' is not assignable to the service type '{1}'",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR016
+ /// Title: Open generic registration is not valid
+ ///
+ public static readonly DiagnosticDescriptor ACTR016_InvalidOpenGenericRegistration = new DiagnosticDescriptor(
+ id: "ACTR016",
+ title: "Open generic registration is not valid",
+ messageFormat: "The open generic service '{0}' cannot be registered with '{1}'; the implementation must be an open generic type with the same number of type parameters",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR017
+ /// Title: Service provider type is not valid
+ ///
+ public static readonly DiagnosticDescriptor ACTR017_InvalidServiceProviderType = new DiagnosticDescriptor(
+ id: "ACTR017",
+ title: "Service provider type is not valid",
+ messageFormat: "The type '{0}' cannot be a service provider; it must not be generic, and must not be marked with [AutoConstruct]",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR020
+ /// Title: Type arguments do not satisfy the open generic implementation
+ ///
+ public static readonly DiagnosticDescriptor ACTR020_OpenGenericConstraintNotSatisfied = new DiagnosticDescriptor(
+ id: "ACTR020",
+ title: "Type arguments do not satisfy the open generic implementation",
+ messageFormat: "'{0}' is needed by '{1}', but its type arguments do not satisfy the constraints of '{2}'",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR021
+ /// Title: Service factory member is not valid
+ ///
+ public static readonly DiagnosticDescriptor ACTR021_InvalidServiceFactory = new DiagnosticDescriptor(
+ id: "ACTR021",
+ title: "Service factory member is not valid",
+ messageFormat: "The factory '{0}' cannot be used for the service '{1}'; it must name an accessible, non-generic method, property or field declared on the service provider that returns a value",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR022
+ /// Title: Service factory does not return the service type
+ ///
+ public static readonly DiagnosticDescriptor ACTR022_ServiceFactoryReturnTypeNotAssignable = new DiagnosticDescriptor(
+ id: "ACTR022",
+ title: "Service factory does not return the service type",
+ messageFormat: "The factory '{0}' returns '{1}', which is not assignable to the service type '{2}'",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR023
+ /// Title: Open generic registration cannot use a factory
+ ///
+ public static readonly DiagnosticDescriptor ACTR023_OpenGenericServiceFactory = new DiagnosticDescriptor(
+ id: "ACTR023",
+ title: "Open generic registration cannot use a factory",
+ messageFormat: "The open generic service '{0}' cannot use the factory '{1}'; a factory builds one type, and an open registration is closed on demand",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR024
+ /// Title: Service provider fallback member is not valid
+ ///
+ public static readonly DiagnosticDescriptor ACTR024_InvalidProviderFallback = new DiagnosticDescriptor(
+ id: "ACTR024",
+ title: "Service provider fallback member is not valid",
+ messageFormat: "The fallback '{0}' cannot be used by '{1}'; it must name an accessible, non-generic field, property or parameterless method declared on the service provider that returns a value",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR025
+ /// Title: Service provider fallback is not a service provider
+ ///
+ public static readonly DiagnosticDescriptor ACTR025_ProviderFallbackNotAServiceProvider = new DiagnosticDescriptor(
+ id: "ACTR025",
+ title: "Service provider fallback is not a service provider",
+ messageFormat: "The fallback '{0}' returns '{1}', which is not assignable to System.IServiceProvider",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR028
+ /// Title: Module factory must be static
+ ///
+ public static readonly DiagnosticDescriptor ACTR028_ModuleFactoryMustBeStatic = new DiagnosticDescriptor(
+ id: "ACTR028",
+ title: "Module factory must be static",
+ messageFormat: "The factory '{0}' on the module '{1}' must be static; a module is never instantiated",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR029
+ /// Title: Service provider type must be sealed
+ ///
+ public static readonly DiagnosticDescriptor ACTR029_ServiceProviderMustBeSealed = new DiagnosticDescriptor(
+ id: "ACTR029",
+ title: "Service provider type must be sealed",
+ messageFormat: "The type '{0}' must be sealed to be a service provider",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ ///
+ /// Id: ACTR030
+ /// Title: Service scan has no filter
+ ///
+ public static readonly DiagnosticDescriptor ACTR030_ScanHasNoFilter = new DiagnosticDescriptor(
+ id: "ACTR030",
+ title: "Service scan has no filter",
+ messageFormat: "'{0}' needs a service or a TypeNameFilter",
+ category: "AutoCtor",
+ DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
}
diff --git a/src/Shared/Helpers/CodeBuilder.Block.cs b/src/Shared/Helpers/CodeBuilder.Block.cs
index 2d52efc8..6cb16155 100644
--- a/src/Shared/Helpers/CodeBuilder.Block.cs
+++ b/src/Shared/Helpers/CodeBuilder.Block.cs
@@ -1,27 +1,30 @@
internal partial class CodeBuilder
{
- public IDisposable StartBlock() => StartIndent("{", "}");
+ public IDisposable StartBlock()
+ {
+ AppendLine("{").IncreaseIndent();
+ return new SingleBlockDisposable(this);
+ }
+
public IDisposable StartBlock(string line)
{
AppendLine(line);
- return StartIndent("{", "}");
+ return StartBlock();
}
- public IDisposable StartIndent(string? startLine = null, string? endLine = null)
+ public IDisposable StartType(string typeDeclaration, IEnumerable? baseList = null)
{
- if (!string.IsNullOrEmpty(startLine))
- AppendLine(startLine!);
+ if (baseList is { })
+ AppendLine($"{typeDeclaration} : {baseList:commaindent}");
+ else
+ AppendLine(typeDeclaration);
+ AppendLine("{");
IncreaseIndent();
- return new DetentDisposable(this, endLine);
+ return new SingleBlockDisposable(this);
}
- private readonly struct DetentDisposable(CodeBuilder codeBuilder, string? endLine) : IDisposable
+ private readonly struct SingleBlockDisposable(CodeBuilder codeBuilder) : IDisposable
{
- public void Dispose()
- {
- codeBuilder.DecreaseIndent();
- if (!string.IsNullOrEmpty(endLine))
- codeBuilder.AppendLine(endLine!);
- }
+ public void Dispose() => codeBuilder.DecreaseIndent().AppendLine("}");
}
}
diff --git a/src/Shared/Helpers/CodeBuilder.InterpolatedStringHandler.cs b/src/Shared/Helpers/CodeBuilder.InterpolatedStringHandler.cs
index b0875840..a31ce596 100644
--- a/src/Shared/Helpers/CodeBuilder.InterpolatedStringHandler.cs
+++ b/src/Shared/Helpers/CodeBuilder.InterpolatedStringHandler.cs
@@ -9,36 +9,24 @@ public CodeBuilder Append(
[InterpolatedStringHandlerArgument("")]
ref CodeBuilderInterpolatedStringHandler builder) => this;
- public CodeBuilder Append(bool enabled,
- [InterpolatedStringHandlerArgument("", nameof(enabled))]
- ref CodeBuilderInterpolatedStringHandler builder) => this;
-
public CodeBuilder AppendLineRaw(
[InterpolatedStringHandlerArgument("")]
ref CodeBuilderInterpolatedStringHandler builder) => AppendLine();
- public CodeBuilder AppendLineRaw(bool enabled,
- [InterpolatedStringHandlerArgument("", nameof(enabled))]
- ref CodeBuilderInterpolatedStringHandler builder) => enabled ? AppendLine() : this;
-
public CodeBuilder AppendLine(
[InterpolatedStringHandlerArgument("")]
IndentedCodeBuilderInterpolatedStringHandler builder) => AppendLine();
- public CodeBuilder AppendLine(bool enabled,
- [InterpolatedStringHandlerArgument("", nameof(enabled))]
- IndentedCodeBuilderInterpolatedStringHandler builder) => enabled ? AppendLine() : this;
-
private void AppendFormatted(IEnumerable items, string? format)
{
if (format == "comma")
- AppendCommaSeparated(items.ToList());
+ AppendCommaSeparated(items as IReadOnlyList ?? items.ToList());
else if (format == "commaindent")
- AppendCommaIndented(items.ToList());
+ AppendCommaIndented(items as IReadOnlyList ?? items.ToList());
}
- private void AppendCommaSeparated(List items)
+ private void AppendCommaSeparated(IReadOnlyList items)
{
var comma = false;
foreach (var item in items)
@@ -50,7 +38,7 @@ private void AppendCommaSeparated(List items)
}
}
- private void AppendCommaIndented(List items)
+ private void AppendCommaIndented(IReadOnlyList items)
{
var length = items.Sum(s => s.Length);
if (length < 60)
@@ -76,19 +64,18 @@ private void AppendCommaIndented(List items)
[InterpolatedStringHandler]
internal readonly struct CodeBuilderInterpolatedStringHandler(
- int literalLength, int formattedCount, CodeBuilder codeBuilder, bool enabled = true)
+ int literalLength, int formattedCount, CodeBuilder codeBuilder)
{
- public readonly bool AppendLiteral(string s)
- { if (enabled) codeBuilder.Append(s); return enabled; }
- public readonly bool AppendFormatted(string s)
- { if (enabled) codeBuilder.Append(s); return enabled; }
- public readonly bool AppendFormatted(IEnumerable items, string? format)
- { if (enabled) codeBuilder.AppendFormatted(items, format); return enabled; }
+ public readonly void AppendLiteral(string s) => codeBuilder.Append(s);
+ public readonly void AppendFormatted(bool s) => codeBuilder.Append(s);
+ public readonly void AppendFormatted(string s) => codeBuilder.Append(s);
+ public readonly void AppendFormatted(IEnumerable items, string? format)
+ => codeBuilder.AppendFormatted(items, format);
}
[InterpolatedStringHandler]
internal sealed class IndentedCodeBuilderInterpolatedStringHandler(
- int literalLength, int formattedCount, CodeBuilder codeBuilder, bool enabled = true)
+ int literalLength, int formattedCount, CodeBuilder codeBuilder)
{
private bool _hasIndented;
@@ -102,11 +89,10 @@ private CodeBuilder EnsureIndent()
return codeBuilder;
}
- public bool AppendLiteral(string s)
- { if (enabled) EnsureIndent().Append(s); return enabled; }
- public bool AppendFormatted(string s)
- { if (enabled) EnsureIndent().Append(s); return enabled; }
- public bool AppendFormatted(IEnumerable items, string? format)
- { if (enabled) EnsureIndent().AppendFormatted(items, format); return enabled; }
+ public void AppendLiteral(string s) => EnsureIndent().Append(s);
+ public void AppendFormatted(bool s) => EnsureIndent().Append(s);
+ public void AppendFormatted(string s) => EnsureIndent().Append(s);
+ public void AppendFormatted(IEnumerable items, string? format)
+ => EnsureIndent().AppendFormatted(items, format);
}
}
diff --git a/src/Shared/Helpers/CodeBuilder.PartialType.cs b/src/Shared/Helpers/CodeBuilder.PartialType.cs
index b1c54e7a..821d2264 100644
--- a/src/Shared/Helpers/CodeBuilder.PartialType.cs
+++ b/src/Shared/Helpers/CodeBuilder.PartialType.cs
@@ -6,7 +6,7 @@
internal partial class CodeBuilder
{
- public IDisposable StartPartialType(IPartialTypeModel typeModel)
+ public IDisposable StartPartialType(IPartialTypeModel typeModel, IEnumerable? baseList = null)
{
if (!string.IsNullOrEmpty(typeModel.Namespace))
{
@@ -17,7 +17,10 @@ public IDisposable StartPartialType(IPartialTypeModel typeModel)
for (var i = 0; i < typeModel.TypeDeclarations.Count; i++)
{
- AppendLine(typeModel.TypeDeclarations[i]);
+ if (i == typeModel.TypeDeclarations.Count - 1 && baseList is { })
+ AppendLine($"{typeModel.TypeDeclarations[i]} : {baseList:commaindent}");
+ else
+ AppendLine(typeModel.TypeDeclarations[i]);
AppendLine("{");
IncreaseIndent();
}
diff --git a/src/Shared/Helpers/CodeBuilder.Templates.cs b/src/Shared/Helpers/CodeBuilder.Templates.cs
index e437c7ca..ee3b1b70 100644
--- a/src/Shared/Helpers/CodeBuilder.Templates.cs
+++ b/src/Shared/Helpers/CodeBuilder.Templates.cs
@@ -26,51 +26,20 @@ static CodeBuilder()
}
}
- // All
- private void AddCompilerGeneratedAttribute()
- => AppendLine("[global::System.Runtime.CompilerServices.CompilerGenerated]");
-
- // All
- private void AddGeneratedCodeAttribute()
+ public CodeBuilder AddGeneratedCodeAttribute()
=> AppendLine($"[global::System.CodeDom.Compiler.GeneratedCode(\"{s_assemblyName}\", \"{s_version}\")]");
- // Class | Struct | Constructor | Method | Property
- private void AddDebuggerNonUserCodeAttribute()
- => AppendLine("[global::System.Diagnostics.DebuggerNonUserCode]");
-
- // Class | Struct | Constructor | Method | Property | Event
- private void AddExcludeFromCodeCoverage()
- => AppendLine($"[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]");
-
- public CodeBuilder AddGeneratedAttributes(AttributeTargets attributeTargets)
+ public CodeBuilder AppendHeader()
{
- AddCompilerGeneratedAttribute();
- AddGeneratedCodeAttribute();
-
- switch (attributeTargets)
- {
- case AttributeTargets.Class:
- case AttributeTargets.Struct:
- case AttributeTargets.Constructor:
- case AttributeTargets.Method:
- case AttributeTargets.Property:
- AddDebuggerNonUserCodeAttribute();
- AddExcludeFromCodeCoverage();
- break;
- case AttributeTargets.Event:
- AddExcludeFromCodeCoverage();
- break;
- }
-
+ AppendLine($"//------------------------------------------------------------------------------");
+ AppendLine($"// ");
+ if (!string.IsNullOrEmpty(s_packageProjectUrl))
+ AppendLine($"// This code was generated by {s_packageProjectUrl!}");
+ AppendLine($"// Version: {s_version}");
+ if (!string.IsNullOrEmpty(s_gitSha))
+ AppendLine($"// SHA: {s_gitSha!}");
+ AppendLine($"// ");
+ AppendLine($"//------------------------------------------------------------------------------");
return this;
}
-
- public CodeBuilder AppendHeader() =>
- AppendLine($"//------------------------------------------------------------------------------")
- .AppendLine($"// ")
- .AppendLine(!string.IsNullOrEmpty(s_packageProjectUrl), $"// This code was generated by {s_packageProjectUrl!}")
- .AppendLine($"// Version: {s_version}")
- .AppendLine(!string.IsNullOrEmpty(s_gitSha), $"// SHA: {s_gitSha!}")
- .AppendLine($"// ")
- .AppendLine($"//------------------------------------------------------------------------------");
}
diff --git a/src/Shared/Helpers/CodeBuilder.cs b/src/Shared/Helpers/CodeBuilder.cs
index 26eb665f..1742d329 100644
--- a/src/Shared/Helpers/CodeBuilder.cs
+++ b/src/Shared/Helpers/CodeBuilder.cs
@@ -6,21 +6,23 @@ internal partial class CodeBuilder
private readonly StringBuilder _stringBuilder = new();
private int _indent;
- public char IndentChar { get; set; } = '\t';
- public string Indent => new(IndentChar, _indent);
+ public char IndentChar = '\t';
+ // Every emitted line asks for this, so the common tab indents are shared, not rebuilt.
+ private static readonly string[] s_tabIndents = [.. Enumerable.Range(0, 16).Select(static i => new string('\t', i))];
+ public string Indent => _indent < s_tabIndents.Length
+ ? s_tabIndents[_indent]
+ : new(IndentChar, _indent);
public CodeBuilder IncreaseIndent() { _indent++; return this; }
public CodeBuilder DecreaseIndent() { if (_indent > 0) _indent--; return this; }
+ public CodeBuilder Append(bool value) { _stringBuilder.Append(value ? "true" : "false"); return this; }
public CodeBuilder Append(string value) { _stringBuilder.Append(value); return this; }
- public CodeBuilder Append(bool enabled, string value) => enabled ? Append(value) : this;
public CodeBuilder AppendLine() { _stringBuilder.AppendLine(); return this; }
public CodeBuilder AppendLine(string value) => AppendLineRaw(Indent + value);
- public CodeBuilder AppendLine(bool enabled, string value) => enabled ? AppendLine(value) : this;
public CodeBuilder AppendLineRaw(string value) { _stringBuilder.AppendLine(value); return this; }
- public CodeBuilder AppendLineRaw(bool enabled, string value) => enabled ? AppendLineRaw(value) : this;
public CodeBuilder AppendIndent() => Append(Indent);
diff --git a/src/Shared/Helpers/EquatableTypeSymbol.cs b/src/Shared/Helpers/EquatableTypeSymbol.cs
index ed2cb6ea..976fafde 100644
--- a/src/Shared/Helpers/EquatableTypeSymbol.cs
+++ b/src/Shared/Helpers/EquatableTypeSymbol.cs
@@ -1,8 +1,12 @@
-using Microsoft.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using Microsoft.CodeAnalysis;
internal readonly record struct EquatableTypeSymbol(ITypeSymbol TypeSymbol)
{
- private readonly string _fullyQualifiedString = TypeSymbol.ToDisplayString(FullyQualifiedFormat);
+ private static readonly ConditionalWeakTable s_displayStrings = new();
+
+ private readonly string _fullyQualifiedString = s_displayStrings.GetValue(
+ TypeSymbol, static t => t.ToDisplayString(FullyQualifiedFormat));
public override int GetHashCode() => ToString().GetHashCode();
public bool Equals(EquatableTypeSymbol other) => EqualityComparer.Default.Equals(ToString(), other.ToString());
diff --git a/src/Shared/Helpers/Extensions.cs b/src/Shared/Helpers/Extensions.cs
index c3b093ce..8c2acd8f 100644
--- a/src/Shared/Helpers/Extensions.cs
+++ b/src/Shared/Helpers/Extensions.cs
@@ -9,6 +9,11 @@
internal static class Extensions
{
+ public static EquatableList ToEquatableList(this IEnumerable source)
+ {
+ return new(source);
+ }
+
public static T? OnlyOrDefault(this IEnumerable source)
{
if (source is IList list)
@@ -46,9 +51,9 @@ internal static class Extensions
"MicrosoftCodeAnalysisCorrectness",
"RS1035:Do not use APIs banned for analyzers",
Justification = "Old generator still maintained")]
- public static void ReportDiagnostic(this EmitterContext context, IHaveDiagnostics item, DiagnosticDescriptor diagnostic)
+ public static void ReportDiagnostic(this EmitterContext context, IHaveDiagnostics item, DiagnosticDescriptor diagnostic, params object?[] messageArgs)
{
foreach (var loc in item.Locations)
- context.ReportDiagnostic(Diagnostic.Create(diagnostic, loc, item.ErrorName));
+ context.ReportDiagnostic(Diagnostic.Create(diagnostic, loc, messageArgs.Length == 0 ? [item.ErrorName] : messageArgs));
}
}
diff --git a/src/Shared/Helpers/GeneratorUtilities.cs b/src/Shared/Helpers/GeneratorUtilities.cs
index affe8236..9cae8a76 100644
--- a/src/Shared/Helpers/GeneratorUtilities.cs
+++ b/src/Shared/Helpers/GeneratorUtilities.cs
@@ -71,4 +71,15 @@ public static bool IsMethodDeclarationWithAttributes(SyntaxNode node, Cancellati
public static TSymbol? GetSymbol(GeneratorSyntaxContext context, CancellationToken cancellationToken) where TSymbol : class, ISymbol
=> context.SemanticModel.GetDeclaredSymbol(context.Node, cancellationToken) as TSymbol;
+
+ // Roslyn 3.11 and 4.0 visit every declaration of a partial type that has attributes, so
+ // only answer for the first of those.
+ public static TSymbol? GetPrimarySymbol(GeneratorSyntaxContext context, CancellationToken cancellationToken) where TSymbol : class, ISymbol
+ {
+ var symbol = GetSymbol(context, cancellationToken);
+ var primary = symbol?.DeclaringSyntaxReferences
+ .Select(r => r.GetSyntax(cancellationToken))
+ .FirstOrDefault(n => IsTypeDeclarationWithAttributes(n, cancellationToken));
+ return primary is null || (primary.SyntaxTree == context.Node.SyntaxTree && primary.Span == context.Node.Span) ? symbol : null;
+ }
}
diff --git a/src/Shared/Models/DuckTypes.cs b/src/Shared/Models/DuckTypes.cs
new file mode 100644
index 00000000..0c8eb5a5
--- /dev/null
+++ b/src/Shared/Models/DuckTypes.cs
@@ -0,0 +1,10 @@
+using Microsoft.CodeAnalysis;
+
+internal readonly record struct DuckTypes(bool DI, bool Keyed, bool Async)
+{
+ public static DuckTypes Create(Compilation c) => new(
+ c.GetTypeByMetadataName("Microsoft.Extensions.DependencyInjection.IServiceProviderIsService") is not null,
+ c.GetTypeByMetadataName("Microsoft.Extensions.DependencyInjection.IKeyedServiceProvider") is not null,
+ c.GetTypeByMetadataName("System.IAsyncDisposable") is not null
+ && c.GetTypeByMetadataName("System.Threading.Tasks.ValueTask") is not null);
+}
diff --git a/src/Shared/Models/ParameterList.cs b/src/Shared/Models/ParameterList.cs
index 82e667a8..72c62bff 100644
--- a/src/Shared/Models/ParameterList.cs
+++ b/src/Shared/Models/ParameterList.cs
@@ -7,7 +7,7 @@
using EmitterContext = Microsoft.CodeAnalysis.SourceProductionContext;
#endif
-internal sealed class ParameterListBuilder(IEnumerable fields, IEnumerable properties)
+internal sealed class ParameterListBuilder(IReadOnlyList fields, IReadOnlyList properties)
{
private IEnumerable _baseParameters = [];
private IEnumerable _postCtorParameters = [];
@@ -20,13 +20,17 @@ public void SetPostCtorParameters(IEnumerable postCtorParameters
public ParameterList Build(EmitterContext context)
{
+ // Keyed by large structs: letting these regrow for a type with hundreds of members
+ // pushes their entry arrays onto the large object heap.
+ var memberCount = fields.Count + properties.Count;
var baseParameters = new List();
var postCtorParameters = new List();
- var parametersMap = new Dictionary();
- var parameterModels = new List();
+ // Keyed by IdentifierName (unique within a type) rather than the whole MemberModel.
+ var parametersMap = new Dictionary(memberCount);
+ var parameterModels = new List(memberCount);
var nameHash = new HashSet();
- var uniqueNames = new Dictionary();
+ var uniqueNames = new Dictionary(memberCount);
foreach (var p in _baseParameters)
{
@@ -39,14 +43,15 @@ public ParameterList Build(EmitterContext context)
foreach (var m in fields)
{
// ref/out from postctor
- if (_postCtorParameters.Any(p => p.IsOutOrRef && m.Type == p.Type))
+ if (IsOutOrRefPostCtorParameter(m))
continue;
var p = ParameterModel.Create(m);
GetUniqueName(p, nameHash, uniqueNames, out var name);
parameterModels.Add(p);
- parametersMap.Add(m, name);
+ // Indexer, not Add: code mid-edit can declare the same member name twice.
+ parametersMap[m.IdentifierName] = name;
}
foreach (var m in properties)
{
@@ -56,7 +61,8 @@ public ParameterList Build(EmitterContext context)
GetUniqueName(p, nameHash, uniqueNames, out var name);
parameterModels.Add(p);
- parametersMap.Add(m, name);
+ // Indexer, not Add: code mid-edit can declare the same member name twice.
+ parametersMap[m.IdentifierName] = name;
}
foreach (var p in _postCtorParameters)
@@ -79,7 +85,9 @@ public ParameterList Build(EmitterContext context)
? $"{p.RefKind.ToParameterPrefix()} {name}" : name);
}
- var constructorParameters = uniqueNames.Select(ConstructorParameterCSharp).ToList();
+ var constructorParameters = new List(uniqueNames.Count);
+ foreach (var u in uniqueNames)
+ constructorParameters.Add(ConstructorParameterCSharp(u));
return new(
constructorParameters,
@@ -90,6 +98,17 @@ public ParameterList Build(EmitterContext context)
);
}
+ // A loop rather than Any(lambda): capturing the member allocated a closure per field.
+ private bool IsOutOrRefPostCtorParameter(MemberModel m)
+ {
+ foreach (var p in _postCtorParameters)
+ {
+ if (p.IsOutOrRef && m.Type == p.Type)
+ return true;
+ }
+ return false;
+ }
+
private static string ConstructorParameterCSharp(KeyValuePair u)
{
var defaultValue = u.Key.HasExplicitDefaultValue
@@ -123,7 +142,7 @@ internal sealed class ParameterList(
IEnumerable ctorParameterDeclarations,
IEnumerable baseParameters,
IEnumerable postCtorParameters,
- Dictionary parameterMap,
+ Dictionary parameterMap,
IEnumerable parameterModels
) : IEnumerable
{
@@ -133,7 +152,7 @@ IEnumerable parameterModels
public IEnumerable