Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ public interface IMyService

The `ServiceLifetime` argument is optional and defaults to [ServiceLifetime.Singleton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicelifetime?#fields).

> NOTE: The attribute is matched by simple name, so you can define your own attribute
> [!NOTE]
> The attribute is matched by simple name, so you can define your own attribute
> in your own assembly. It only has to provide a constructor receiving a
> [ServiceLifetime](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.servicelifetime) argument,
> and optionally an overload receiving an `object key` for keyed services.
Expand All @@ -77,13 +78,27 @@ app.MapGet("/", (IMyService service) => service.Message);
app.Run();
```

> NOTE: the service is available automatically for the scoped request, because
> [!NOTE]
> The service is available automatically for the scoped request, because
> we called the generated `AddServices` that registers the discovered services.

And that's it. The source generator will discover annotated types in the current
project and all its references too. Since the registration code is generated at
compile-time, there is no run-time reflection (or dependencies) whatsoever.

If the service implements many interfaces and you want to register it only for
a specific one, you can specify that as the generic argument:

```csharp
[Service<IMyService>(ServiceLifetime.Scoped)]
public class MyService : IMyService, IDisposable
```

> [!TIP]
> If no specific interface is provided, all implemented interfaces are registered
> for the same service implementation (and they all resolve to the same instance,
> except for transient lifetime).

### Convention-based

You can also avoid attributes entirely by using a convention-based approach, which
Expand Down Expand Up @@ -156,6 +171,7 @@ right `INotificationService` will be injected, based on the key provided.
Note you can also register the same service using multiple keys, as shown in the
`EmailNotificationService` above.

> [!IMPORTANT]
> Keyed services are a feature of version 8.0+ of Microsoft.Extensions.DependencyInjection

## How It Works
Expand All @@ -180,7 +196,8 @@ other two registrations just retrieve the same service (according to its defined
lifetime). This means the instance is reused and properly registered under
all implemented interfaces automatically.

> NOTE: you can inspect the generated code by setting `EmitCompilerGeneratedFiles=true`
> [!TIP]
> You can inspect the generated code by setting `EmitCompilerGeneratedFiles=true`
> in your project file and browsing the `generated` subfolder under `obj`.

If the service type has dependencies, they will be resolved from the service
Expand Down Expand Up @@ -262,9 +279,11 @@ public class ServiceAttribute : Attribute
}
```

> NOTE: since the constructor arguments are only used by the source generation to

> [!TIP]
> Since the constructor arguments are only used by the source generation to
> detemine the registration style (and key), but never at run-time, you don't even need
> to keep it around in a field or property!
> to keep them around in a field or property!

With this in place, you only need to add this package to the top-level project
that is adding the services to the collection!
Expand Down
25 changes: 22 additions & 3 deletions src/DependencyInjection.Tests/GenerationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,17 @@ public void RegisterWithCustomServiceAttribute()
Assert.Same(instance, services.GetRequiredService<IAsyncDisposable>());
}

[Fact]
public void RegisterWithSpecificServiceType()
{
var collection = new ServiceCollection();
collection.AddServices();
var services = collection.BuildServiceProvider();

Assert.NotNull(services.GetRequiredService<ISpecificService>());
Assert.Null(services.GetService<INonSpecificService>());
}

[GenerationTests.Service(ServiceLifetime.Singleton)]
public class MyAttributedService : IAsyncDisposable
{
Expand Down Expand Up @@ -380,11 +391,19 @@ public class SmsNotificationService : INotificationService
}

// Showcases that legacy generic Service<TKey> attribute still works
// but now with new semantics enforced by an analyzer.
[Service("email")]
#pragma warning disable CS0618 // Type or member is obsolete
[Service<string>("default")]
#pragma warning restore CS0618 // Type or member is obsolete
[Service<INotificationService>("default")]
public class EmailNotificationService : INotificationService
{
public string Notify(string message) => $"[Email] {message}";
}

public interface ISpecificService;
public interface INonSpecificService;

[Service<ISpecificService>]
public class SpecificServiceType : ISpecificService, INonSpecificService
{
public void Dispose() => throw new NotImplementedException();
}
3 changes: 1 addition & 2 deletions src/DependencyInjection/AddServicesAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ namespace Devlooped.Extensions.DependencyInjection;
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public class AddServicesAnalyzer : DiagnosticAnalyzer
{
public static DiagnosticDescriptor NoAddServicesCall { get; } =
new DiagnosticDescriptor(
public static DiagnosticDescriptor NoAddServicesCall { get; } = new DiagnosticDescriptor(
"DDI001",
"No call to IServiceCollection.AddServices found.",
"The AddServices extension method must be invoked in order for discovered services to be properly registered.",
Expand Down
10 changes: 9 additions & 1 deletion src/DependencyInjection/CodeAnalysisExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System.Diagnostics;
using Microsoft.CodeAnalysis.Diagnostics;

static class CodeAnalysisExtensions
Expand All @@ -7,13 +7,21 @@ static class CodeAnalysisExtensions
/// Gets whether the current build is a design-time build.
/// </summary>
public static bool IsDesignTimeBuild(this AnalyzerConfigOptionsProvider options) =>
#if DEBUG
// Assume if we have a debugger attached to a debug build, we want to debug the generator
!Debugger.IsAttached &&
#endif
options.GlobalOptions.TryGetValue("build_property.DesignTimeBuild", out var value) &&
bool.TryParse(value, out var isDesignTime) && isDesignTime;

/// <summary>
/// Gets whether the current build is a design-time build.
/// </summary>
public static bool IsDesignTimeBuild(this AnalyzerConfigOptions options) =>
#if DEBUG
// Assume if we have a debugger attached to a debug build, we want to debug the generator
!Debugger.IsAttached &&
#endif
options.TryGetValue("build_property.DesignTimeBuild", out var value) &&
bool.TryParse(value, out var isDesignTime) && isDesignTime;
}
6 changes: 2 additions & 4 deletions src/DependencyInjection/ConventionsAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,15 @@ namespace Devlooped.Extensions.DependencyInjection;
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public class ConventionsAnalyzer : DiagnosticAnalyzer
{
public static DiagnosticDescriptor AssignableTypeOfRequired { get; } =
new DiagnosticDescriptor(
public static DiagnosticDescriptor AssignableTypeOfRequired { get; } = new DiagnosticDescriptor(
"DDI002",
"The convention-based registration requires a typeof() expression.",
"When registering services by type, typeof() must be used exclusively to avoid run-time reflection.",
"Build",
DiagnosticSeverity.Error,
isEnabledByDefault: true);

public static DiagnosticDescriptor OpenGenericType { get; } =
new DiagnosticDescriptor(
public static DiagnosticDescriptor OpenGenericType { get; } = new DiagnosticDescriptor(
"DDI003",
"Open generic service implementations are not supported for convention-based registration.",
"Only the concrete (closed) implementations of the open generic interface will be registered. Register open generic services explicitly using the built-in service collection methods.",
Expand Down
Loading