diff --git a/src/DependencyModules.SourceGenerator.Impl/Utilities/ServiceModelUtility.cs b/src/DependencyModules.SourceGenerator.Impl/Utilities/ServiceModelUtility.cs index 4dcc8ff..8c783c4 100644 --- a/src/DependencyModules.SourceGenerator.Impl/Utilities/ServiceModelUtility.cs +++ b/src/DependencyModules.SourceGenerator.Impl/Utilities/ServiceModelUtility.cs @@ -520,7 +520,17 @@ CancellationToken cancellationToken { var list = new List(); - foreach (var attributeSyntax in context.Node.DescendantNodes().OfType()) + // The declaration's own attributes only. An attribute on a nested class or on a factory + // method in this class registers that class or that method, not this one. + var attributeLists = context.Node is MemberDeclarationSyntax member + ? member.AttributeLists + : default; + + foreach ( + var attributeSyntax in attributeLists.SelectMany(attributeList => + attributeList.Attributes + ) + ) { foreach (var typeDefinition in _attributeTypes) { diff --git a/tests/DependencyModules.Tests/GeneratorTests/NestedServiceAttributeTests.cs b/tests/DependencyModules.Tests/GeneratorTests/NestedServiceAttributeTests.cs new file mode 100644 index 0000000..4b74725 --- /dev/null +++ b/tests/DependencyModules.Tests/GeneratorTests/NestedServiceAttributeTests.cs @@ -0,0 +1,81 @@ +using DependencyModules.Tests.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace DependencyModules.Tests.GeneratorTests; + +/// +/// A service attribute registers the declaration it is written on. A nested class or a factory +/// method inside a service class has its own registration and does not add one for the class. +/// +public class NestedServiceAttributeTests +{ + [Fact] + public void ANestedServiceClass_DoesNotRegisterItsContainingClassAgain() + { + var assembly = GeneratedAssembly.Create( + """ + using DependencyModules.Runtime.Attributes; + + namespace TestNamespace; + + public interface ICache { } + + public interface IEntry { } + + [SingletonService] + public class Cache : ICache + { + [TransientService] + public class Entry : IEntry { } + } + + [DependencyModule] + public partial class TestModule; + """ + ); + + var cache = assembly.Descriptor("ICache"); + + Assert.Equal(ServiceLifetime.Singleton, cache.Lifetime); + Assert.Equal(ServiceLifetime.Transient, assembly.Descriptor("IEntry").Lifetime); + + var provider = assembly.BuildProvider(); + + Assert.Same( + provider.GetRequiredService(assembly.Type("ICache")), + provider.GetRequiredService(assembly.Type("ICache")) + ); + } + + [Fact] + public void AFactoryMethodInAServiceClass_DoesNotRegisterItsContainingClassAgain() + { + var assembly = GeneratedAssembly.Create( + """ + using DependencyModules.Runtime.Attributes; + + namespace TestNamespace; + + public interface IStore { } + + public interface IClock { } + + public class Clock : IClock { } + + [SingletonService] + public class Store : IStore + { + [ScopedService] + public static IClock CreateClock() => new Clock(); + } + + [DependencyModule] + public partial class TestModule; + """ + ); + + Assert.Equal(ServiceLifetime.Singleton, assembly.Descriptor("IStore").Lifetime); + Assert.Equal(ServiceLifetime.Scoped, assembly.Descriptor("IClock").Lifetime); + } +}