Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
24 changes: 17 additions & 7 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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 = _
Expand Down Expand Up @@ -226,17 +227,26 @@ 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
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}]
Expand Down
5 changes: 4 additions & 1 deletion AutoCtor.slnx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<Solution>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
<File Path="global.json" />
<File Path="mdsnippets.json" />
<File Path="readme.nuget.md" />
Expand Down Expand Up @@ -30,7 +31,9 @@
<Project Path="src/AutoCtor.Roslyn3.11/AutoCtor.Roslyn3.11.csproj" />
<Project Path="src/AutoCtor.Roslyn4.0/AutoCtor.Roslyn4.0.csproj" />
<Project Path="src/AutoCtor.Roslyn4.4/AutoCtor.Roslyn4.4.csproj" />
<Project Path="src/Benchmarks/AutoCtor.Benchmarks.csproj" />
<Project Path="src/Benchmarks/Benchmarks.csproj" />
<Project Path="src/CompileBenchmarks/CompileBenchmarks.csproj" Id="7517ad45-4734-4722-9fd1-05d68a6b1949" />
<Project Path="src/Shared/Shared.csproj" />
<Project Path="src/Tests/Tests.csproj" />
<Project Path="src/WebApiExample/WebApiExample.csproj" Id="1e16ec8d-af29-44f2-85c8-f17cc7c4a0ba" />
</Solution>
2,129 changes: 2,066 additions & 63 deletions readme.md

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions readme.nuget.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<IClock, SystemClock>]
[Scoped<IUnitOfWork, UnitOfWork>]
[Transient<IReportBuilder, ReportBuilder>]
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<T>` 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.
95 changes: 88 additions & 7 deletions readme.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -171,6 +175,88 @@ snippet: Keyed.cs#Keyed.g.verified.cs

</details>

<a href='#toc' title='Back to Contents'>Back to Contents</a>
## Service Provider

snippet: ServiceProvider

<details><summary>What gets generated</summary>

snippet: ServiceProvider.cs#Container.ServiceProvider.g.verified.cs

</details>

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<T>`. 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

<details><summary>What gets generated</summary>

snippet: ServiceProviderScan.cs#OrderContainer.ServiceProvider.g.verified.cs

</details>

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

<details><summary>What gets generated</summary>

snippet: ServiceProviderKeyed.cs#PaymentContainer.ServiceProvider.g.verified.cs

</details>

### Factories

snippet: ServiceProviderFactory

<details><summary>What gets generated</summary>

snippet: ServiceProviderFactory.cs#HostContainer.ServiceProvider.g.verified.cs

</details>

### Falling back to another provider

`Fallback` on `[ServiceProvider]` names a member to use when there is no registration for that service.

snippet: ServiceProviderFallback

<details><summary>What gets generated</summary>

snippet: ServiceProviderFallback.cs#FallbackContainer.ServiceProvider.g.verified.cs

</details>

### Modules

`[Import<TModule>]` (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

<details><summary>What gets generated</summary>

snippet: ServiceProviderModule.cs#TestContainer.ServiceProvider.g.verified.cs

</details>

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.

<a href='#toc' title='Back to Contents'>Back to Contents</a>
## Other

Expand Down Expand Up @@ -200,6 +286,8 @@ Your project file should look like this:
</Project>
```

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`.

<details><summary>What gets generated</summary>

snippet: GeneratedAttributeTests.cs#AutoConstructAttribute.g.verified.cs
Expand All @@ -224,10 +312,3 @@ If you wish to preserve these attributes in the build output, add the define con
```

<a href='#toc' title='Back to Contents'>Back to Contents</a>
## Star History

[![Star History Chart](https://api.star-history.com/svg?repos=distantcam/autoctor&type=date&legend=top-left)](https://www.star-history.com/#distantcam/autoctor&type=date&legend=top-left)

## Stats

![Alt](https://repobeats.axiom.co/api/embed/8d02b2c004a5f958b4365abad3d4d1882dca200f.svg "Repobeats analytics image")
134 changes: 134 additions & 0 deletions src/AutoCtor.Attributes/IoCAttributes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System.Diagnostics;
using static System.AttributeTargets;

namespace AutoCtor;

/// <summary>
/// Generates a compile time <see cref="IServiceProvider"/> from the registration attributes on this class.
/// </summary>
[AttributeUsage(Class, Inherited = false)]
[Conditional("AUTOCTOR_USAGES")]
public sealed class ServiceProviderAttribute : Attribute
{
/// <summary>
/// A field, property or parameterless method (named with <c>nameof</c>) returning an
/// <see cref="IServiceProvider"/> to ask for anything not registered.
/// </summary>
public string? Fallback { get; set; }
}

public abstract class ServiceAttribute : Attribute
{
public object? Key { get; set; }

/// <summary>
/// A method, property or field (named with <c>nameof</c>) that builds the service instead
/// of its constructor. A method's parameters are resolved like constructor parameters.
/// </summary>
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<TService> : ServiceAttribute;

[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
[Conditional("AUTOCTOR_USAGES")]
public sealed class SingletonAttribute<TService, TImplementation> : 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<TService> : ServiceAttribute;

[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
[Conditional("AUTOCTOR_USAGES")]
public sealed class TransientAttribute<TService, TImplementation> : 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<TService> : ServiceAttribute;

[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
[Conditional("AUTOCTOR_USAGES")]
public sealed class ScopedAttribute<TService, TImplementation> : ServiceAttribute;

/// <summary>
/// Adds every registration, scan and import on another class, in place. A module in another
/// assembly is only seen when that assembly defines <c>AUTOCTOR_USAGES</c>.
/// </summary>
[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
[Conditional("AUTOCTOR_USAGES")]
public sealed class ImportAttribute(Type module) : Attribute
{
public Type Module { get; } = module;
}

/// <inheritdoc cref="ImportAttribute"/>
[AttributeUsage(Class, AllowMultiple = true, Inherited = false)]
[Conditional("AUTOCTOR_USAGES")]
public sealed class ImportAttribute<TModule> : Attribute;

[Flags]
public enum ScanAs
{
/// <summary>The scanned for type, closed over the match for an open generic.</summary>
Service = 1,
/// <summary>The implementation itself.</summary>
Self = 2,
/// <summary>Every interface of the implementation.</summary>
ImplementedInterfaces = 4,
}

/// <summary>
/// Registers every accessible, concrete, non-generic class assignable to <see cref="Service"/>,
/// and matching <see cref="TypeNameFilter"/>. At least one of the two is required.
/// </summary>
public abstract class ScanAttribute(Type? service) : Attribute
{
/// <summary>
/// An open generic such as <c>typeof(IHandler&lt;&gt;)</c> matches every construction of it.
/// Without one, <see cref="ScanAs.Service"/> registers each type as itself.
/// </summary>
public Type? Service { get; } = service;
public ScanAs As { get; set; } = ScanAs.Service;
/// <summary>Scans the assemblies of these types instead of the provider's own.</summary>
public Type[]? FromAssembliesOf { get; set; }
/// <summary>Only types whose name matches, where <c>*</c> matches anything and <c>?</c> any one character, such as <c>"*Repository"</c>.</summary>
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);
18 changes: 0 additions & 18 deletions src/AutoCtor.Example/AutoCtor.Example.csproj

This file was deleted.

Loading
Loading