diff --git a/readme.md b/readme.md index 8708435..c84ee1e 100644 --- a/readme.md +++ b/readme.md @@ -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. @@ -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(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 @@ -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 @@ -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 @@ -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! diff --git a/src/DependencyInjection.Tests/GenerationTests.cs b/src/DependencyInjection.Tests/GenerationTests.cs index 292af6e..9cb9379 100644 --- a/src/DependencyInjection.Tests/GenerationTests.cs +++ b/src/DependencyInjection.Tests/GenerationTests.cs @@ -272,6 +272,17 @@ public void RegisterWithCustomServiceAttribute() Assert.Same(instance, services.GetRequiredService()); } + [Fact] + public void RegisterWithSpecificServiceType() + { + var collection = new ServiceCollection(); + collection.AddServices(); + var services = collection.BuildServiceProvider(); + + Assert.NotNull(services.GetRequiredService()); + Assert.Null(services.GetService()); + } + [GenerationTests.Service(ServiceLifetime.Singleton)] public class MyAttributedService : IAsyncDisposable { @@ -380,11 +391,19 @@ public class SmsNotificationService : INotificationService } // Showcases that legacy generic Service attribute still works +// but now with new semantics enforced by an analyzer. [Service("email")] -#pragma warning disable CS0618 // Type or member is obsolete -[Service("default")] -#pragma warning restore CS0618 // Type or member is obsolete +[Service("default")] public class EmailNotificationService : INotificationService { public string Notify(string message) => $"[Email] {message}"; +} + +public interface ISpecificService; +public interface INonSpecificService; + +[Service] +public class SpecificServiceType : ISpecificService, INonSpecificService +{ + public void Dispose() => throw new NotImplementedException(); } \ No newline at end of file diff --git a/src/DependencyInjection/AddServicesAnalyzer.cs b/src/DependencyInjection/AddServicesAnalyzer.cs index 607c8be..339ba28 100644 --- a/src/DependencyInjection/AddServicesAnalyzer.cs +++ b/src/DependencyInjection/AddServicesAnalyzer.cs @@ -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.", diff --git a/src/DependencyInjection/CodeAnalysisExtensions.cs b/src/DependencyInjection/CodeAnalysisExtensions.cs index dde45f7..55000f2 100644 --- a/src/DependencyInjection/CodeAnalysisExtensions.cs +++ b/src/DependencyInjection/CodeAnalysisExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics; static class CodeAnalysisExtensions @@ -7,6 +7,10 @@ static class CodeAnalysisExtensions /// Gets whether the current build is a design-time build. /// 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; @@ -14,6 +18,10 @@ public static bool IsDesignTimeBuild(this AnalyzerConfigOptionsProvider options) /// Gets whether the current build is a design-time build. /// 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; } \ No newline at end of file diff --git a/src/DependencyInjection/ConventionsAnalyzer.cs b/src/DependencyInjection/ConventionsAnalyzer.cs index 47cf44a..6b9b766 100644 --- a/src/DependencyInjection/ConventionsAnalyzer.cs +++ b/src/DependencyInjection/ConventionsAnalyzer.cs @@ -10,8 +10,7 @@ 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.", @@ -19,8 +18,7 @@ public class ConventionsAnalyzer : DiagnosticAnalyzer 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.", diff --git a/src/DependencyInjection/IncrementalGenerator.cs b/src/DependencyInjection/IncrementalGenerator.cs index 7f35a7c..1a8acba 100644 --- a/src/DependencyInjection/IncrementalGenerator.cs +++ b/src/DependencyInjection/IncrementalGenerator.cs @@ -10,7 +10,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Extensions.DependencyInjection; -using KeyedService = (Microsoft.CodeAnalysis.INamedTypeSymbol Type, Microsoft.CodeAnalysis.TypedConstant? Key); +using KeyedService = (Microsoft.CodeAnalysis.INamedTypeSymbol TImplementation, Microsoft.CodeAnalysis.INamedTypeSymbol? TService, Microsoft.CodeAnalysis.TypedConstant? Key); namespace Devlooped.Extensions.DependencyInjection; @@ -30,9 +30,10 @@ public class IncrementalGenerator : IIncrementalGenerator DiagnosticSeverity.Warning, isEnabledByDefault: true); - class ServiceSymbol(INamedTypeSymbol type, int lifetime, TypedConstant? key, Location? location) + class ServiceSymbol(INamedTypeSymbol implementation, int lifetime, TypedConstant? key, Location? location, INamedTypeSymbol? service) { - public INamedTypeSymbol Type => type; + public INamedTypeSymbol TImplementation => implementation; + public INamedTypeSymbol? TService => service; public int Lifetime => lifetime; public TypedConstant? Key => key; public Location? Location => location; @@ -42,13 +43,20 @@ public override bool Equals(object? obj) if (obj is not ServiceSymbol other) return false; - return type.Equals(other.Type, SymbolEqualityComparer.Default) && + return SymbolEqualityComparer.Default.Equals(implementation, other.TImplementation) && + SymbolEqualityComparer.Default.Equals(service, other.TService) && lifetime == other.Lifetime && - Equals(key, other); + Equals(key, other.Key); } public override int GetHashCode() - => HashCode.Combine(SymbolEqualityComparer.Default.GetHashCode(type), lifetime, key); + { + var hashcode = HashCode.Combine(SymbolEqualityComparer.Default.GetHashCode(implementation), lifetime, key); + if (service != null) + hashcode = HashCode.Combine(hashcode, SymbolEqualityComparer.Default.GetHashCode(service)); + + return hashcode; + } } record ServiceRegistration(int Lifetime, TypeSyntax? AssignableTo, string? FullNameExpression, Location? Location) @@ -110,10 +118,13 @@ bool IsExport(AttributeData attr) // more flexible and avoids requiring any sort of run-time dependency. var attributedServices = types - .SelectMany((x, _) => + .Where(x => x.GetAttributes().Any()) + .Combine(context.CompilationProvider) + .SelectMany((x, cancellation) => { - var name = x.Name; - var attrs = x.GetAttributes(); + var (type, compilation) = x; + var name = type.Name; + var attrs = type.GetAttributes(); var services = new List(); foreach (var attr in attrs) @@ -164,7 +175,21 @@ bool IsExport(AttributeData attr) } } - services.Add(new(x, lifetime, key, attr.ApplicationSyntaxReference?.GetSyntax().GetLocation())); + INamedTypeSymbol? serviceType = null; + + if (serviceAttr?.AttributeClass?.Arity == 1 && serviceAttr.ApplicationSyntaxReference != null && + serviceAttr.ApplicationSyntaxReference.GetSyntax(cancellation) is AttributeSyntax serviceAttrSyntax && + compilation.GetSemanticModel(serviceAttr.ApplicationSyntaxReference.SyntaxTree).GetSymbolInfo(serviceAttrSyntax, cancellation) is { Symbol: not null } serviceAttrSymbol && + serviceAttrSymbol.Symbol is IMethodSymbol attrCtor && + attrCtor.ContainingType.IsGenericType && + attrCtor.ContainingType.TypeArguments.Length == 1 && + attrCtor.ContainingType.TypeArguments[0] is INamedTypeSymbol attrServiceType) + { + // We have a specific service type to register. + serviceType = attrServiceType; + } + + services.Add(new(type, lifetime, key, attr.ApplicationSyntaxReference?.GetSyntax().GetLocation(), serviceType)); } return services.ToImmutableArray(); @@ -209,7 +234,7 @@ bool IsExport(AttributeData attr) if (registration!.FullNameExpression != null && !registration.Regex.IsMatch(typeSymbol.ToFullName(compilation))) continue; - results.Add(new ServiceSymbol(typeSymbol, registration.Lifetime, null, registration.Location)); + results.Add(new ServiceSymbol(typeSymbol, registration.Lifetime, null, registration.Location, null)); } return results.ToImmutable(); @@ -226,27 +251,27 @@ bool IsExport(AttributeData attr) void RegisterServicesOutput(IncrementalGeneratorInitializationContext context, IncrementalValuesProvider services, IncrementalValueProvider compilation) { context.RegisterImplementationSourceOutput( - services.Where(x => x!.Lifetime == 0 && x.Key is null).Select((x, _) => new KeyedService(x!.Type, null)).Collect().Combine(compilation), + services.Where(x => x!.Lifetime == 0 && x.Key is null).Select((x, _) => new KeyedService(x!.TImplementation, x!.TService, null)).Collect().Combine(compilation), (ctx, data) => AddPartial("AddSingleton", ctx, data)); context.RegisterImplementationSourceOutput( - services.Where(x => x!.Lifetime == 1 && x.Key is null).Select((x, _) => new KeyedService(x!.Type, null)).Collect().Combine(compilation), + services.Where(x => x!.Lifetime == 1 && x.Key is null).Select((x, _) => new KeyedService(x!.TImplementation, x!.TService, null)).Collect().Combine(compilation), (ctx, data) => AddPartial("AddScoped", ctx, data)); context.RegisterImplementationSourceOutput( - services.Where(x => x!.Lifetime == 2 && x.Key is null).Select((x, _) => new KeyedService(x!.Type, null)).Collect().Combine(compilation), + services.Where(x => x!.Lifetime == 2 && x.Key is null).Select((x, _) => new KeyedService(x!.TImplementation, x!.TService, null)).Collect().Combine(compilation), (ctx, data) => AddPartial("AddTransient", ctx, data)); context.RegisterImplementationSourceOutput( - services.Where(x => x!.Lifetime == 0 && x.Key is not null).Select((x, _) => new KeyedService(x!.Type, x.Key!)).Collect().Combine(compilation), + services.Where(x => x!.Lifetime == 0 && x.Key is not null).Select((x, _) => new KeyedService(x!.TImplementation, x!.TService, x.Key!)).Collect().Combine(compilation), (ctx, data) => AddPartial("AddKeyedSingleton", ctx, data)); context.RegisterImplementationSourceOutput( - services.Where(x => x!.Lifetime == 1 && x.Key is not null).Select((x, _) => new KeyedService(x!.Type, x.Key!)).Collect().Combine(compilation), + services.Where(x => x!.Lifetime == 1 && x.Key is not null).Select((x, _) => new KeyedService(x!.TImplementation, x!.TService, x.Key!)).Collect().Combine(compilation), (ctx, data) => AddPartial("AddKeyedScoped", ctx, data)); context.RegisterImplementationSourceOutput( - services.Where(x => x!.Lifetime == 2 && x.Key is not null).Select((x, _) => new KeyedService(x!.Type, x.Key!)).Collect().Combine(compilation), + services.Where(x => x!.Lifetime == 2 && x.Key is not null).Select((x, _) => new KeyedService(x!.TImplementation, x!.TService, x.Key!)).Collect().Combine(compilation), (ctx, data) => AddPartial("AddKeyedTransient", ctx, data)); context.RegisterImplementationSourceOutput(services.Collect(), ReportInconsistencies); @@ -254,7 +279,7 @@ void RegisterServicesOutput(IncrementalGeneratorInitializationContext context, I void ReportInconsistencies(SourceProductionContext context, ImmutableArray array) { - var grouped = array.GroupBy(x => x.Type, SymbolEqualityComparer.Default).Where(g => g.Count() > 1).ToImmutableArray(); + var grouped = array.GroupBy(x => x.TImplementation, SymbolEqualityComparer.Default).Where(g => g.Count() > 1).ToImmutableArray(); if (grouped.Length == 0) return; @@ -274,7 +299,7 @@ void ReportInconsistencies(SourceProductionContext context, ImmutableArray x.Location != null).Skip(1).Select(x => x.Location!); context.ReportDiagnostic(Diagnostic.Create(AmbiguousLifetime, - location, otherLocations, keyed.First().Type.ToDisplayString(), string.Join(", ", lifetimes))); + location, otherLocations, keyed.First().TImplementation.ToDisplayString(), string.Join(", ", lifetimes))); } } } @@ -363,7 +388,7 @@ static partial class AddServicesNoReflectionExtension { """); - AddServices(data.Types.Where(x => x.Key is null).Select(x => x.Type), data.Compilation, methodName, builder); + AddServices(data.Types.Where(x => x.Key is null), data.Compilation, methodName, builder); AddKeyedServices(data.Types.Where(x => x.Key is not null), data.Compilation, methodName, builder); builder.AppendLine( @@ -376,12 +401,13 @@ static partial class AddServicesNoReflectionExtension ctx.AddSource(methodName + ".g", builder.ToString().Replace("\r\n", "\n").Replace("\n", Environment.NewLine)); } - void AddServices(IEnumerable services, Compilation compilation, string methodName, StringBuilder output) + void AddServices(IEnumerable services, Compilation compilation, string methodName, StringBuilder output) { bool isAccessible(ISymbol s) => compilation.IsSymbolAccessible(s); - foreach (var type in services) + foreach (var info in services) { + var type = info.TImplementation; var impl = type.ToFullName(compilation); var registered = new HashSet(); @@ -415,7 +441,9 @@ void AddServices(IEnumerable services, Compilation compilation output.AppendLine($" services.AddTransient>(s => s.GetRequiredService<{impl}>);"); output.AppendLine($" services.AddTransient(s => new Lazy<{impl}>(s.GetRequiredService<{impl}>));"); - foreach (var iface in type.AllInterfaces) + var serviceTypes = info.TService != null ? ImmutableArray.Create(info.TService) : type.AllInterfaces; + + foreach (var iface in serviceTypes) { if (!compilation.HasImplicitConversion(type, iface)) continue; @@ -468,18 +496,19 @@ void AddKeyedServices(IEnumerable services, Compilation compilatio { bool isAccessible(ISymbol s) => compilation.IsSymbolAccessible(s); - foreach (var type in services) + foreach (var info in services) { - var impl = type.Type.ToFullName(compilation); + var type = info.TImplementation; + var impl = type.ToFullName(compilation); var registered = new HashSet(); - var key = type.Key!.Value.ToCSharpString(); + var key = info.Key!.Value.ToCSharpString(); - var importing = type.Type.InstanceConstructors.FirstOrDefault(m => + var importing = type.InstanceConstructors.FirstOrDefault(m => m.GetAttributes().Any(a => a.AttributeClass?.ToFullName(compilation) == "global::System.Composition.ImportingConstructorAttribute" || a.AttributeClass?.ToFullName(compilation) == "global::System.ComponentModel.Composition.ImportingConstructorAttribute")); - var ctor = importing ?? type.Type.InstanceConstructors + var ctor = importing ?? type.InstanceConstructors .Where(isAccessible) .OrderByDescending(m => m.Parameters.Length) .FirstOrDefault(); @@ -504,7 +533,9 @@ void AddKeyedServices(IEnumerable services, Compilation compilatio output.AppendLine($" services.AddKeyedTransient>({key}, (s, k) => () => s.GetRequiredKeyedService<{impl}>(k));"); output.AppendLine($" services.AddKeyedTransient({key}, (s, k) => new Lazy<{impl}>(() => s.GetRequiredKeyedService<{impl}>(k)));"); - foreach (var iface in type.Type.AllInterfaces) + var serviceTypes = info.TService != null ? ImmutableArray.Create(info.TService) : info.TImplementation.AllInterfaces; + + foreach (var iface in serviceTypes) { var ifaceName = iface.ToFullName(compilation); if (!registered.Contains(ifaceName)) diff --git a/src/DependencyInjection/LegacyServiceAttributeAnalyzer.cs b/src/DependencyInjection/LegacyServiceAttributeAnalyzer.cs new file mode 100644 index 0000000..8179e2c --- /dev/null +++ b/src/DependencyInjection/LegacyServiceAttributeAnalyzer.cs @@ -0,0 +1,70 @@ +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Devlooped.Extensions.DependencyInjection; + +[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] +public class LegacyServiceAttributeAnalyzer : DiagnosticAnalyzer +{ + public static DiagnosticDescriptor ServiceTypeNotKeyType { get; } = new DiagnosticDescriptor( + "DDI005", + "Generic parameter for ServiceAttribute must be the service type to register, not the service key type.", + "Generic parameter must not be the type of service key in use but rather the service type to register, if any.", + "Build", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(ServiceTypeNotKeyType); + + public override void Initialize(AnalysisContext context) + { + if (!Debugger.IsAttached) + context.EnableConcurrentExecution(); + + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.RegisterSymbolAction(AnalyzeNamedType, SymbolKind.NamedType); + } + + void AnalyzeNamedType(SymbolAnalysisContext context) + { + var typeSymbol = (INamedTypeSymbol)context.Symbol; + foreach (var attribute in typeSymbol.GetAttributes()) + { + if (attribute.AttributeClass is not { IsGenericType: true } attrClass) + continue; + + if (attrClass.Name != "ServiceAttribute" && attrClass.Name != "Service") + continue; + + if (attrClass.TypeArguments.Length != 1) + continue; + + var typeArgument = attrClass.TypeArguments[0]; + // Registering as generic object should be ok. + if (typeArgument.SpecialType == SpecialType.System_Object) + continue; + + if (attribute.ConstructorArguments.Length == 0) + continue; + + var keyArg = attribute.ConstructorArguments[0]; + + // If they match, this would be the legacy usage scenario we want to fix + if (keyArg.Type is not null && + SymbolEqualityComparer.Default.Equals(keyArg.Type, typeArgument)) + { + var location = attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation() + ?? typeSymbol.Locations.FirstOrDefault(); + + if (location is not null) + { + context.ReportDiagnostic(Diagnostic.Create(ServiceTypeNotKeyType, location)); + } + } + } + } +} diff --git a/src/DependencyInjection/Properties/launchSettings.json b/src/DependencyInjection/Properties/launchSettings.json index bba7c6a..7efec7d 100644 --- a/src/DependencyInjection/Properties/launchSettings.json +++ b/src/DependencyInjection/Properties/launchSettings.json @@ -3,6 +3,10 @@ "Tests": { "commandName": "DebugRoslynComponent", "targetProject": "..\\DependencyInjection.Tests\\DependencyInjection.Tests.csproj" + }, + "NoAddService": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\NoAddServices\\NoAddServices.csproj" } } } \ No newline at end of file diff --git a/src/DependencyInjection/compile/ServiceAttribute.cs b/src/DependencyInjection/compile/ServiceAttribute.cs index 96cc264..7587aa8 100644 --- a/src/DependencyInjection/compile/ServiceAttribute.cs +++ b/src/DependencyInjection/compile/ServiceAttribute.cs @@ -7,6 +7,11 @@ namespace Microsoft.Extensions.DependencyInjection /// /// Configures the registration of a service in an . /// + /// + /// Registers the service using all interfaces implemented by the target, + /// as well as Func{TInterface} and Lazy{TInterface} for each, + /// so the instance can be lazily retrieved. + /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] partial class ServiceAttribute : Attribute { diff --git a/src/DependencyInjection/compile/ServiceAttribute`1.cs b/src/DependencyInjection/compile/ServiceAttribute`1.cs index b296b48..2fea5fa 100644 --- a/src/DependencyInjection/compile/ServiceAttribute`1.cs +++ b/src/DependencyInjection/compile/ServiceAttribute`1.cs @@ -4,17 +4,26 @@ namespace Microsoft.Extensions.DependencyInjection { /// - /// Configures the registration of a keyed service in an . - /// Requires v8 or later of Microsoft.Extensions.DependencyInjection package. + /// Configures the registration of a service in an using + /// a specific service type. /// - /// Type of service key. + /// + /// Registers the service using the specific service interface, + /// as well as Func<TService> and Lazy<TService>, + /// so the instance can be lazily retrieved. + /// + /// Type of service to register. [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] - [Obsolete("Use ServiceAttribute(object key, ServiceLifetime lifetime) instead.")] - partial class ServiceAttribute : Attribute + partial class ServiceAttribute : Attribute { /// /// Annotates the service with the lifetime. /// - public ServiceAttribute(TKey key, ServiceLifetime lifetime = ServiceLifetime.Singleton) { } + public ServiceAttribute(ServiceLifetime lifetime = ServiceLifetime.Singleton) { } + + /// + /// Annotates the service with the given key and lifetime. + /// + public ServiceAttribute(object key, ServiceLifetime lifetime = ServiceLifetime.Singleton) { } } } \ No newline at end of file