diff --git a/.gitignore b/.gitignore index dfcfd56..4fe5636 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ # Mono auto generated files mono_crash.* +*.received.txt + # Build results [Dd]ebug/ [Dd]ebugPublic/ @@ -40,7 +42,7 @@ bld/ Generated\ Files/ # MSTest test Results -[Tt]est[Rr]esult*/ +[Tt]est[Rr]esult*(?!verified.txt) [Bb]uild[Ll]og.* # NUnit diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Contracts/Attributes/DecorateAttribute.cs b/DependencyInjection.SourceGenerator.Contracts/Attributes/DecorateAttribute.cs deleted file mode 100644 index fab9351..0000000 --- a/DependencyInjection.SourceGenerator.Contracts/Attributes/DecorateAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -using DependencyInjection.SourceGenerator.Contracts.Enums; -using System; - -namespace DependencyInjection.SourceGenerator.Contracts.Attributes; - -[AttributeUsage(AttributeTargets.Class)] -public class DecorateAttribute : Attribute -{ - public Type? ServiceType { get; set; } -} - -[AttributeUsage(AttributeTargets.Class)] -public class DecorateAttribute : Attribute -{ -} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Contracts/Attributes/RegisterAllAttribute.cs b/DependencyInjection.SourceGenerator.Contracts/Attributes/RegisterAllAttribute.cs deleted file mode 100644 index 85fb985..0000000 --- a/DependencyInjection.SourceGenerator.Contracts/Attributes/RegisterAllAttribute.cs +++ /dev/null @@ -1,32 +0,0 @@ -using DependencyInjection.SourceGenerator.Contracts.Attributes; -using DependencyInjection.SourceGenerator.Contracts.Enums; - -namespace DependencyInjection.SourceGenerator.Contracts.Attributes; - -public interface IRegisterAllAttribute -{ - Lifetime Lifetime { get;} - bool IncludeServiceName { get; } -} - -[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] -public class RegisterAllAttribute(Type serviceType) : Attribute, IRegisterAllAttribute -{ - public Lifetime Lifetime { get; set; } = Lifetime.Transient; - public Type ServiceType { get; set; } = serviceType; - - public bool IncludeServiceName { get; set; } - - public RegisterAllAttribute(Type serviceType, Lifetime lifetime) : this(serviceType) - { - Lifetime = lifetime; - } -} - - -[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] -public class RegisterAllAttribute : Attribute, IRegisterAllAttribute -{ - public Lifetime Lifetime { get; set; } = Lifetime.Transient; - public bool IncludeServiceName { get; set; } -} diff --git a/DependencyInjection.SourceGenerator.Contracts/Attributes/RegisterAttribute.cs b/DependencyInjection.SourceGenerator.Contracts/Attributes/RegisterAttribute.cs deleted file mode 100644 index 374317f..0000000 --- a/DependencyInjection.SourceGenerator.Contracts/Attributes/RegisterAttribute.cs +++ /dev/null @@ -1,24 +0,0 @@ -using DependencyInjection.SourceGenerator.Contracts.Enums; -using System; - -namespace DependencyInjection.SourceGenerator.Contracts.Attributes; -public interface IRegisterAttribute -{ - public Lifetime Lifetime { get; } - public string? ServiceName { get; } -} - -[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] -public class RegisterAttribute : Attribute, IRegisterAttribute -{ - public Lifetime Lifetime { get; set; } = Lifetime.Transient; - public string? ServiceName { get; set; } - public Type? ServiceType { get; set; } -} - -[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] -public class RegisterAttribute : Attribute, IRegisterAttribute -{ - public Lifetime Lifetime { get; set; } = Lifetime.Transient; - public string? ServiceName { get; set; } -} diff --git a/DependencyInjection.SourceGenerator.Contracts/DependencyInjection.SourceGenerator.Contracts.csproj b/DependencyInjection.SourceGenerator.Contracts/DependencyInjection.SourceGenerator.Contracts.csproj deleted file mode 100644 index 95b9d44..0000000 --- a/DependencyInjection.SourceGenerator.Contracts/DependencyInjection.SourceGenerator.Contracts.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - - netstandard2.0 - SourceGeneration - Contains types used to set up dependency injection registrations - true - - - - diff --git a/DependencyInjection.SourceGenerator.Contracts/Enums/Lifetime.cs b/DependencyInjection.SourceGenerator.Contracts/Enums/Lifetime.cs deleted file mode 100644 index f43fbc8..0000000 --- a/DependencyInjection.SourceGenerator.Contracts/Enums/Lifetime.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace DependencyInjection.SourceGenerator.Contracts.Enums; - -public enum Lifetime -{ - Transient, - Scoped, - Singleton, -} diff --git a/DependencyInjection.SourceGenerator.LightInject.Contracts/Attributes/RegisterCompositionRootAttribute.cs b/DependencyInjection.SourceGenerator.LightInject.Contracts/Attributes/RegisterCompositionRootAttribute.cs deleted file mode 100644 index f27b6c6..0000000 --- a/DependencyInjection.SourceGenerator.LightInject.Contracts/Attributes/RegisterCompositionRootAttribute.cs +++ /dev/null @@ -1,7 +0,0 @@ - -namespace DependencyInjection.SourceGenerator.LightInject.Contracts.Attributes; - -[AttributeUsage(AttributeTargets.Class)] -public class RegisterCompositionRootAttribute : Attribute -{ -} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.LightInject.Contracts/DependencyInjection.SourceGenerator.LightInject.Contracts.csproj b/DependencyInjection.SourceGenerator.LightInject.Contracts/DependencyInjection.SourceGenerator.LightInject.Contracts.csproj deleted file mode 100644 index b5267e4..0000000 --- a/DependencyInjection.SourceGenerator.LightInject.Contracts/DependencyInjection.SourceGenerator.LightInject.Contracts.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net48;netstandard2.0;net6.0;net7.0;net8.0 - SourceGeneration - Contains types used to set up dependency injection registrations for LightInject - true - - - - - - - diff --git a/DependencyInjection.SourceGenerator.LightInject.Tests/CSharpSourceGeneratorVerifier.cs b/DependencyInjection.SourceGenerator.LightInject.Tests/CSharpSourceGeneratorVerifier.cs deleted file mode 100644 index 96d7b3a..0000000 --- a/DependencyInjection.SourceGenerator.LightInject.Tests/CSharpSourceGeneratorVerifier.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.CodeAnalysis.CSharp.Testing; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis; -using System.Collections.Immutable; -using Microsoft.CodeAnalysis.Testing; - -namespace DependencyInjection.SourceGenerator.LightInject.Tests; - -public static class CSharpSourceGeneratorVerifier - where TSourceGenerator : ISourceGenerator, new() -{ - public class Test : CSharpSourceGeneratorTest - { - public Test() - { - } - - protected override CompilationOptions CreateCompilationOptions() - { - var compilationOptions = base.CreateCompilationOptions(); - return compilationOptions.WithSpecificDiagnosticOptions( - compilationOptions.SpecificDiagnosticOptions.SetItems(GetNullableWarningsFromCompiler())); - } - - public LanguageVersion LanguageVersion { get; set; } = LanguageVersion.Default; - - private static ImmutableDictionary GetNullableWarningsFromCompiler() - { - string[] args = { "/warnaserror:nullable" }; - var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory); - var nullableWarnings = commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; - - return nullableWarnings; - } - - protected override ParseOptions CreateParseOptions() - { - return ((CSharpParseOptions)base.CreateParseOptions()).WithLanguageVersion(LanguageVersion); - } - } -} diff --git a/DependencyInjection.SourceGenerator.LightInject.Tests/DependencyInjection.SourceGenerator.LightInject.Tests.csproj b/DependencyInjection.SourceGenerator.LightInject.Tests/DependencyInjection.SourceGenerator.LightInject.Tests.csproj deleted file mode 100644 index 5ce74d1..0000000 --- a/DependencyInjection.SourceGenerator.LightInject.Tests/DependencyInjection.SourceGenerator.LightInject.Tests.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - net8.0 - enable - enable - - false - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - diff --git a/DependencyInjection.SourceGenerator.LightInject.Tests/DependencyInjectionRegistrationGeneratorTests.cs b/DependencyInjection.SourceGenerator.LightInject.Tests/DependencyInjectionRegistrationGeneratorTests.cs deleted file mode 100644 index ebf7db1..0000000 --- a/DependencyInjection.SourceGenerator.LightInject.Tests/DependencyInjectionRegistrationGeneratorTests.cs +++ /dev/null @@ -1,451 +0,0 @@ -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis; -using System.Collections.Immutable; -using System.Reflection; -using Microsoft.CodeAnalysis.Text; -using System.Text; -using VerifyCS = DependencyInjection.SourceGenerator.LightInject.Tests.CSharpSourceGeneratorVerifier; -using Microsoft.CodeAnalysis.Testing; - -namespace DependencyInjection.SourceGenerator.LightInject.Tests; - -public class DependencyInjectionRegistrationGeneratorTests -{ - private static readonly string _header = """ - // - #pragma warning disable - #nullable enable - namespace DependencyInjection.SourceGenerator.LightInject.Demo; - [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - - """; - - private readonly ImmutableArray _references = AppDomain.CurrentDomain - .GetAssemblies() - .Where(assembly => !assembly.IsDynamic) - .Select(assembly => assembly.Location) - .ToImmutableArray(); - - private async Task RunTestAsync(string code, string expectedResult) - { - var net8 = new ReferenceAssemblies( - "net8.0", - new PackageIdentity( - "Microsoft.NETCore.App.Ref", - "8.0.0"), - Path.Combine("ref", "net8.0")); - - var tester = new VerifyCS.Test - { - TestState = - { - Sources = { code }, - GeneratedSources = - { - (typeof(DependencyInjectionRegistrationGenerator), "CompositionRoot.g.cs", - SourceText.From(expectedResult, Encoding.UTF8)) - } - }, - ReferenceAssemblies = net8 - }; - - tester.ReferenceAssemblies.AddAssemblies(_references); - tester.TestState.AdditionalReferences.Add(typeof(global::DependencyInjection.SourceGenerator.Contracts.Attributes.RegisterAllAttribute).Assembly); - tester.TestState.AdditionalReferences.Add(typeof(Contracts.Attributes.RegisterCompositionRootAttribute).Assembly); - tester.TestState.AdditionalReferences.Add(typeof(global::LightInject.IServiceContainer).Assembly); - - await tester.RunAsync(); - } - - [Fact] - public async Task CreateCompositionRoot_RegisterService_NoExistingCompositionRoot() - { - var code = """ -using DependencyInjection.SourceGenerator.Contracts.Attributes; - -namespace DependencyInjection.SourceGenerator.LightInject.Demo; - -[Register] -public class Service : IService {} -public interface IService {} - -"""; - - var expected = _header + """ -public class CompositionRoot : global::LightInject.ICompositionRoot -{ - public void Compose(global::LightInject.IServiceRegistry serviceRegistry) - { - serviceRegistry.Register(new global::LightInject.PerRequestLifeTime()); - } -} -"""; - - await RunTestAsync(code, expected); - Assert.True(true); // silence warnings, real test happens in the RunAsync() method - } - - [Fact] - public async Task CreateCompositionRoot_RegisterService_NoExistingCompositionRoot_MultipleRegistrations() - { - var code = """ -using DependencyInjection.SourceGenerator.Contracts.Attributes; - -namespace DependencyInjection.SourceGenerator.LightInject.Demo; - -[Register(ServiceType = typeof(IService1))] -[Register(ServiceType = typeof(IService2))] -public class Service : IService1, IService2 {} -public interface IService1 {} -public interface IService2 {} - -"""; - - var expected = _header + """ -public class CompositionRoot : global::LightInject.ICompositionRoot -{ - public void Compose(global::LightInject.IServiceRegistry serviceRegistry) - { - serviceRegistry.Register(new global::LightInject.PerRequestLifeTime()); - serviceRegistry.Register(new global::LightInject.PerRequestLifeTime()); - } -} -"""; - - await RunTestAsync(code, expected); - Assert.True(true); // silence warnings, real test happens in the RunAsync() method - } - - [Fact] - public async Task CreateCompositionRoot_RegisterService_ExistingCompositionRoot() - { - var code = """ -using DependencyInjection.SourceGenerator.Contracts.Attributes; -using LightInject; - -namespace DependencyInjection.SourceGenerator.LightInject.Demo; - -[Register] -public class Service : IService {} -public interface IService {} - -public partial class CompositionRoot : global::LightInject.ICompositionRoot -{ - public static void RegisterServices(global::LightInject.IServiceRegistry serviceRegistry) - { - - } -} - -"""; - - var expected = _header + """ -public partial class CompositionRoot : global::LightInject.ICompositionRoot -{ - public void Compose(global::LightInject.IServiceRegistry serviceRegistry) - { - RegisterServices(serviceRegistry); - serviceRegistry.Register(new global::LightInject.PerRequestLifeTime()); - } -} -"""; - - await RunTestAsync(code, expected); - Assert.True(true); // silence warnings, real test happens in the RunAsync() method - } - - [Fact] - public async Task Register_SpecifiedLifetime_And_ServiceName() - { - var code = """ -using DependencyInjection.SourceGenerator.Contracts.Attributes; -using DependencyInjection.SourceGenerator.Contracts.Enums; - -namespace DependencyInjection.SourceGenerator.LightInject.Demo; - -[Register(Lifetime = Lifetime.Scoped, ServiceName = "Test")] -public class Service : IService {} -public interface IService {} - -"""; - - var expected = _header + """ -public class CompositionRoot : global::LightInject.ICompositionRoot -{ - public void Compose(global::LightInject.IServiceRegistry serviceRegistry) - { - serviceRegistry.Register("Test", new global::LightInject.PerScopeLifetime()); - } -} -"""; - - await RunTestAsync(code, expected); - Assert.True(true); // silence warnings, real test happens in the RunAsync() method - } - - [Fact] - public async Task Register_Specified_ServiceType() - { - var code = """ -using DependencyInjection.SourceGenerator.Contracts.Attributes; -using DependencyInjection.SourceGenerator.Contracts.Enums; - -namespace DependencyInjection.SourceGenerator.LightInject.Demo; - -[Register(ServiceType = typeof(Service))] -public class Service : IService {} -public interface IService {} - -"""; - - var expected = _header + """ -public class CompositionRoot : global::LightInject.ICompositionRoot -{ - public void Compose(global::LightInject.IServiceRegistry serviceRegistry) - { - serviceRegistry.Register(new global::LightInject.PerRequestLifeTime()); - } -} -"""; - - await RunTestAsync(code, expected); - Assert.True(true); // silence warnings, real test happens in the RunAsync() method - } - - [Fact] - public async Task Register_NoInteface() - { - var code = """ -using DependencyInjection.SourceGenerator.Contracts.Attributes; -using DependencyInjection.SourceGenerator.Contracts.Enums; - -namespace DependencyInjection.SourceGenerator.LightInject.Demo; - -[Register] -public class Service {} - -"""; - - var expected = _header + """ -public class CompositionRoot : global::LightInject.ICompositionRoot -{ - public void Compose(global::LightInject.IServiceRegistry serviceRegistry) - { - serviceRegistry.Register(new global::LightInject.PerRequestLifeTime()); - } -} -"""; - - await RunTestAsync(code, expected); - Assert.True(true); // silence warnings, real test happens in the RunAsync() method - } - - [Fact] - public async Task CreateCompositionRoot_DecorateService_NoExistingCompositionRoot() - { - var code = """ -using DependencyInjection.SourceGenerator.Contracts.Attributes; - -namespace DependencyInjection.SourceGenerator.LightInject.Demo; - -[Decorate] -public class Service : IService {} -public interface IService {} - -"""; - - var expected = _header + """ -public class CompositionRoot : global::LightInject.ICompositionRoot -{ - public void Compose(global::LightInject.IServiceRegistry serviceRegistry) - { - serviceRegistry.Decorate(); - } -} -"""; - - await RunTestAsync(code, expected); - Assert.True(true); // silence warnings, real test happens in the RunAsync() method - } - - [Fact] - public async Task RegisterAll_ByInterface() - { - var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; - -[assembly: RegisterAll] - -namespace DependencyInjection.SourceGenerator.LightInject.Demo; - -public class Service1 : IService {} -public class Service2 : IService {} -public interface IService {} - -"""; - - var expected = _header + """ -public class CompositionRoot : global::LightInject.ICompositionRoot -{ - public void Compose(global::LightInject.IServiceRegistry serviceRegistry) - { - serviceRegistry.Register(new global::LightInject.PerRequestLifeTime()); - serviceRegistry.Register(new global::LightInject.PerRequestLifeTime()); - } -} -"""; - - await RunTestAsync(code, expected); - } - - [Fact] - public async Task RegisterAll_SpecifyLifetime() - { - var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; -using global::DependencyInjection.SourceGenerator.Contracts.Enums; - -[assembly: RegisterAll(typeof(global::DependencyInjection.SourceGenerator.LightInject.Demo.IService<>), Lifetime.Singleton)] - -namespace DependencyInjection.SourceGenerator.LightInject.Demo; - -public class Service1 : IService {} -public class Service2 : IService {} -public interface IService {} - -"""; - - var expected = _header + """ -public class CompositionRoot : global::LightInject.ICompositionRoot -{ - public void Compose(global::LightInject.IServiceRegistry serviceRegistry) - { - serviceRegistry.Register, global::DependencyInjection.SourceGenerator.LightInject.Demo.Service2>(new global::LightInject.PerContainerLifetime()); - serviceRegistry.Register, global::DependencyInjection.SourceGenerator.LightInject.Demo.Service1>(new global::LightInject.PerContainerLifetime()); - } -} -"""; - - await RunTestAsync(code, expected); - } - - [Fact] - public async Task RegisterAll_ByBaseType_WithServiceName() - { - var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; - -[assembly: RegisterAll(IncludeServiceName = true)] - -namespace DependencyInjection.SourceGenerator.LightInject.Demo; - -public class Service1 : MyBase {} -public class Service2 : MyBase {} -public abstract class MyBase {} - -"""; - - var expected = _header + """ -public class CompositionRoot : global::LightInject.ICompositionRoot -{ - public void Compose(global::LightInject.IServiceRegistry serviceRegistry) - { - serviceRegistry.Register("Service2", new global::LightInject.PerRequestLifeTime()); - serviceRegistry.Register("Service1", new global::LightInject.PerRequestLifeTime()); - } -} -"""; - - await RunTestAsync(code, expected); - } - - [Fact] - public async Task RegisterAll_ByBaseType_WithoutServiceName() - { - var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; - -[assembly: RegisterAll] - -namespace DependencyInjection.SourceGenerator.LightInject.Demo; - -public class Service1 : MyBase {} -public class Service2 : MyBase {} -public abstract class MyBase {} - -"""; - - var expected = _header + """ -public class CompositionRoot : global::LightInject.ICompositionRoot -{ - public void Compose(global::LightInject.IServiceRegistry serviceRegistry) - { - serviceRegistry.Register(new global::LightInject.PerRequestLifeTime()); - serviceRegistry.Register(new global::LightInject.PerRequestLifeTime()); - } -} -"""; - - await RunTestAsync(code, expected); - } - - [Fact] - public async Task RegisterCompositionRoot() - { - var code = """ -using global::DependencyInjection.SourceGenerator.LightInject.Contracts.Attributes; -using LightInject; - -namespace DependencyInjection.SourceGenerator.LightInject.Demo; - -[RegisterCompositionRoot] -public class CustomCompositionRoot : ICompositionRoot -{ - public void Compose(IServiceRegistry serviceRegistry) - { - } - -} - -"""; - - var expected = _header + """ -public class CompositionRoot : global::LightInject.ICompositionRoot -{ - public void Compose(global::LightInject.IServiceRegistry serviceRegistry) - { - serviceRegistry.RegisterFrom(); - } -} -"""; - - await RunTestAsync(code, expected); - } - - [Fact] - public async Task RegisterRecord() - { - var code = """ - -using DependencyInjection.SourceGenerator.Contracts.Attributes; - -namespace DependencyInjection.SourceGenerator.LightInject.Demo; - -[Register] -public record Service(); - -"""; - - var expected = _header + """ -public class CompositionRoot : global::LightInject.ICompositionRoot -{ - public void Compose(global::LightInject.IServiceRegistry serviceRegistry) - { - serviceRegistry.Register(new global::LightInject.PerRequestLifeTime()); - } -} -"""; - - await RunTestAsync(code, expected); - } -} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.LightInject.Tests/Usings.cs b/DependencyInjection.SourceGenerator.LightInject.Tests/Usings.cs deleted file mode 100644 index 8c927eb..0000000 --- a/DependencyInjection.SourceGenerator.LightInject.Tests/Usings.cs +++ /dev/null @@ -1 +0,0 @@ -global using Xunit; \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.LightInject/DependencyInjection.SourceGenerator.LightInject.csproj b/DependencyInjection.SourceGenerator.LightInject/DependencyInjection.SourceGenerator.LightInject.csproj deleted file mode 100644 index 4c2faa2..0000000 --- a/DependencyInjection.SourceGenerator.LightInject/DependencyInjection.SourceGenerator.LightInject.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - netstandard2.0 - true - readme.md - - - - SourceGenerator;LightInject - Generates dependency injection registration using source generation - true - - false - true - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - diff --git a/DependencyInjection.SourceGenerator.LightInject/DependencyInjectionRegistrationGenerator.cs b/DependencyInjection.SourceGenerator.LightInject/DependencyInjectionRegistrationGenerator.cs deleted file mode 100644 index 5d8a3cd..0000000 --- a/DependencyInjection.SourceGenerator.LightInject/DependencyInjectionRegistrationGenerator.cs +++ /dev/null @@ -1,299 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Text; -using System.Text; -using DependencyInjection.SourceGenerator.LightInject.Contracts.Attributes; -using DependencyInjection.SourceGenerator.Contracts.Enums; -using DependencyInjection.SourceGenerator.Shared; - -namespace DependencyInjection.SourceGenerator.LightInject; - -[Generator] -public class DependencyInjectionRegistrationGenerator : ISourceGenerator -{ - public void Initialize(GeneratorInitializationContext context) - { - context.RegisterForSyntaxNotifications(() => new ClassAttributeReceiver(additionalClassAttributes: [nameof(RegisterCompositionRootAttribute)])); - } - - public void Execute(GeneratorExecutionContext context) - { - // Get first existing CompositionRoot class - var compositionRoot = context.Compilation.SyntaxTrees - .SelectMany(x => x.GetRoot().DescendantNodes()) - .OfType() - .FirstOrDefault(x => x.Identifier.Text == "CompositionRoot"); - - if (compositionRoot is not null && !IsPartial(compositionRoot)) - { - var descriptor = new DiagnosticDescriptor("DIL01", "CompositionRoot not patial", "CompositionRoot must be partial", "LightInject", DiagnosticSeverity.Error, true); - context.ReportDiagnostic(Diagnostic.Create(descriptor, compositionRoot.GetLocation())); - return; - } - - // If CompositionRoot class exists, get namespace, if not, use root namespace of project - var @namespace = GetDefaultNamespace(context, compositionRoot); - - var classesToRegister = RegistrationCollector.GetTypes(context); - var registerAllTypes = RegistrationCollector.GetRegisterAllTypes(context); - - var source = GenerateCompositionRoot(context, compositionRoot is not null, @namespace, classesToRegister, registerAllTypes); - var sourceText = source.ToFullString(); - context.AddSource("CompositionRoot.g.cs", SourceText.From(sourceText, Encoding.UTF8)); - } - - internal static string GetDefaultNamespace(GeneratorExecutionContext context, ClassDeclarationSyntax? compositionRoot) - { - if (compositionRoot?.Parent is NamespaceDeclarationSyntax namespaceDeclarationSyntax) - return namespaceDeclarationSyntax.Name.ToString(); - - if (compositionRoot?.Parent is FileScopedNamespaceDeclarationSyntax fileScopedNamespaceDeclarationSyntax) - return fileScopedNamespaceDeclarationSyntax.Name.ToString(); - - var @namespace = context.Compilation.SyntaxTrees - .SelectMany(x => x.GetRoot().DescendantNodes()) - .OfType() - .Select(x => x.Name.ToString()) - .Min(); - - if (@namespace is not null) - return @namespace; - - @namespace = context.Compilation.SyntaxTrees - .SelectMany(x => x.GetRoot().DescendantNodes()) - .OfType() - .Select(x => x.Name.ToString()) - .Min(); - - if (@namespace is not null) - return @namespace; - - throw new NotSupportedException("Unable to calculate namespace"); - } - - private static CompilationUnitSyntax GenerateCompositionRoot(GeneratorExecutionContext context, bool userDefinedCompositionRoot, string @namespace, IEnumerable classesToRegister, List additionalRegistrations) - { - var modifiers = SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword)); - - if (userDefinedCompositionRoot) - modifiers = modifiers.Add(SyntaxFactory.Token(SyntaxKind.PartialKeyword)); - - var classModifiers = SyntaxFactory.TokenList(modifiers); - - var bodyMembers = new List(); - if (userDefinedCompositionRoot) - bodyMembers.Add(CreateRegisterServicesCall()); - - foreach (var type in classesToRegister) - { - var registrations = RegistrationMapper.CreateRegistration(type); - foreach (var registration in registrations) - { - if (registrations is not null) - bodyMembers.Add(CreateServiceRegistration(registration.ServiceType, registration.ImplementationTypeName, registration.Lifetime, registration.ServiceName)); - } - - var decoration = DecorationMapper.CreateDecoration(type); - if (decoration is not null) - bodyMembers.Add(CreateServiceDecoration(decoration.DecoratedTypeName, decoration.DecoratorTypeName)); - - var registrationSyntax = CreateRegistrationExtensions(context, type); - if (registrationSyntax is not null) - bodyMembers.Add(registrationSyntax); - - } - - foreach (var registration in additionalRegistrations) - { - bodyMembers.Add(CreateServiceRegistration(registration.ServiceType, registration.ImplementationTypeName, registration.Lifetime, registration.ServiceName)); - } - - var body = SyntaxFactory.Block(bodyMembers.ToArray()); - - - var serviceRegistryType = CreateServiceRegistrySyntax("IServiceRegistry"); - - var methodParameter = SyntaxFactory.Parameter(SyntaxFactory.Identifier("serviceRegistry")) - .WithType(serviceRegistryType); - - var methodDeclaration = SyntaxFactory.MethodDeclaration(SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)), SyntaxFactory.Identifier("Compose")) - .AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword)) - .AddParameterListParameters(methodParameter) - .WithBody(body); - - var compositionRootSyntax = CreateServiceRegistrySyntax("ICompositionRoot"); - var baseType = SyntaxFactory.SimpleBaseType(compositionRootSyntax); - - var classDeclaration = SyntaxFactory.ClassDeclaration("CompositionRoot") - .WithModifiers(classModifiers) - .AddBaseListTypes(baseType) - .AddMembers(methodDeclaration); - - return Trivia.CreateCompilationUnitSyntax(classDeclaration, @namespace); - } - - internal static ExpressionStatementSyntax? CreateRegistrationExtensions(GeneratorExecutionContext context, INamedTypeSymbol type) - { - var attribute = TypeHelper.GetAttributes(type.GetAttributes()).FirstOrDefault(); - if (attribute is null) - return null; - - if (!TypeImplementsCompositionRoot(type)) - { - var diagnostic = Diagnostic.Create( - new DiagnosticDescriptor( - "DIL0002", - "Invalid composition root implementation", - "Class {0} does not implement ICompositionRoot", - "InvalidConfig", - DiagnosticSeverity.Error, - true), null, type.Name); - context.ReportDiagnostic(diagnostic); - return null; - } - - return CreateRegisterFromSyntax(type); - } - - private static ExpressionStatementSyntax? CreateRegisterFromSyntax(INamedTypeSymbol type) - { - var typeName = TypeHelper.GetFullName(type); - var invocationExpression = SyntaxFactory.InvocationExpression( - SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.IdentifierName("serviceRegistry"), - SyntaxFactory.GenericName( - SyntaxFactory.Identifier("RegisterFrom")) - .WithTypeArgumentList( - SyntaxFactory.TypeArgumentList( - SyntaxFactory.SingletonSeparatedList( - SyntaxFactory.IdentifierName(typeName)))))); - - return SyntaxFactory.ExpressionStatement(invocationExpression); - } - - private static bool TypeImplementsCompositionRoot(INamedTypeSymbol type) - { - return type.AllInterfaces.Any(x => TypeHelper.GetFullName(x) == "global::LightInject.ICompositionRoot"); - } - - private static ExpressionStatementSyntax CreateServiceDecoration(string decoratedTypeName, string decoratorTypeName) - { - SyntaxNodeOrToken[] tokens = - [ - SyntaxFactory.IdentifierName(decoratedTypeName), - SyntaxFactory.Token(SyntaxKind.CommaToken), - SyntaxFactory.IdentifierName(decoratorTypeName) - ]; - - var accessExpression = SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.IdentifierName("serviceRegistry"), - SyntaxFactory.GenericName( - SyntaxFactory.Identifier("Decorate")) - .WithTypeArgumentList( - SyntaxFactory.TypeArgumentList( - SyntaxFactory.SeparatedList(tokens)))); - - var argumentList = SyntaxFactory.ArgumentList(); - - var expression = SyntaxFactory.InvocationExpression(accessExpression) - .WithArgumentList(argumentList); - - return SyntaxFactory.ExpressionStatement(expression); - } - - private static ExpressionStatementSyntax CreateRegisterServicesCall() - { - return SyntaxFactory.ExpressionStatement( - SyntaxFactory.InvocationExpression( - SyntaxFactory.IdentifierName("RegisterServices")) - .WithArgumentList( - SyntaxFactory.ArgumentList( - SyntaxFactory.SingletonSeparatedList( - SyntaxFactory.Argument( - SyntaxFactory.IdentifierName("serviceRegistry")))))); - } - - private static ExpressionStatementSyntax CreateServiceRegistration(string? serviceType, string implementation, Lifetime lifetime, string? serviceName) - { - var lifetimeName = lifetime switch - { - Lifetime.Singleton => "PerContainerLifetime", - Lifetime.Scoped => "PerScopeLifetime", - Lifetime.Transient => "PerRequestLifeTime", - _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) - }; - - - var args = new List(); - if (!string.IsNullOrEmpty(serviceName)) - { - var serviceNameSyntax = SyntaxFactory.Argument( - SyntaxFactory.LiteralExpression( - SyntaxKind.StringLiteralExpression, - SyntaxFactory.Literal(serviceName!))); - - args.Add(serviceNameSyntax); - args.Add(SyntaxFactory.Token(SyntaxKind.CommaToken)); - } - - var lifetimeIdentifierSyntax = CreateServiceRegistrySyntax(lifetimeName); - var lifetimeSyntaxArgument = SyntaxFactory.Argument( - SyntaxFactory.ObjectCreationExpression(lifetimeIdentifierSyntax) - .WithArgumentList(SyntaxFactory.ArgumentList())); - - args.Add(lifetimeSyntaxArgument); - - var argumentList = SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(args)); - - SyntaxNodeOrToken[] tokens; - if (serviceType is null) - { - tokens = [SyntaxFactory.IdentifierName(implementation)]; - } - else - { - tokens = - [ - SyntaxFactory.IdentifierName(serviceType), - SyntaxFactory.Token(SyntaxKind.CommaToken), - SyntaxFactory.IdentifierName(implementation) - ]; - } - - return SyntaxFactory.ExpressionStatement( - SyntaxFactory.InvocationExpression( - SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.IdentifierName("serviceRegistry"), - SyntaxFactory.GenericName( - SyntaxFactory.Identifier("Register")) - .WithTypeArgumentList( - SyntaxFactory.TypeArgumentList( - SyntaxFactory.SeparatedList( - tokens))))) - .WithArgumentList(argumentList)); - } - - private static bool IsPartial(ClassDeclarationSyntax compositionRoot) - { - return compositionRoot.Modifiers.Any(x => x.Text == "partial"); - } - - internal static QualifiedNameSyntax CreateServiceRegistrySyntax(string className) - { - return SyntaxFactory.QualifiedName( - SyntaxFactory.AliasQualifiedName( - SyntaxFactory.IdentifierName( - SyntaxFactory.Token(SyntaxKind.GlobalKeyword)), - SyntaxFactory.IdentifierName("LightInject")), - SyntaxFactory.IdentifierName(className)); - - //var attribute = SyntaxFactory.Attribute(name); - //var attributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(attribute)); - - //return SyntaxFactory.SingletonList(attributeList); - } -} diff --git a/DependencyInjection.SourceGenerator.LightInject/README.md b/DependencyInjection.SourceGenerator.LightInject/README.md deleted file mode 100644 index 5445bd5..0000000 --- a/DependencyInjection.SourceGenerator.LightInject/README.md +++ /dev/null @@ -1,120 +0,0 @@ -# DependencyInjection.SourceGenerator.LightInject -Register services using attributes instead of registering in code. - -## Usage -Add the "Register" attribute to the class you want to register. The attribute takes a type and a lifetime. The type is the type you want to register and the lifetime is the lifetime of the service. The lifetime is optional and defaults to Transient. - -```csharp -var services = new ServiceCollection(); -services.AddMyProject(); - -### LightInject - -```csharp -[Register(ServiceName = "ServiceName", Lifetime = Lifetime.Singleton)] -public class ExampleService : IExampleService -{ - public string GetExample() - { - return "Example"; - } -} - -public interface IExampleService -{ - string GetExample(); -} - -``` - -Generates a class CompositionRoot - -```csharp -public class CompositionRoot : ICompositionRoot -{ - public static void Compose(IServiceRegistry serviceRegistry) - { - serviceRegistry.Register("ServiceName", new PerContainerLifetime()); - } -} -``` - -If you already have a class CompositionRoot defined, the generated class will be made partial. Remeber to make your CompositionRoot partial as well. -It will also call a method RegisterServices on the existing CompositionRoot class (this must be defined). - -```csharp -public partial class CompositionRoot : ICompositionRoot -{ - public static void Compose(IServiceRegistry serviceRegistry) - { - RegisterServices(serviceRegistry); - serviceRegistry.Register("ServiceName", new PerContainerLifetime()); - } -} -``` - -The final existing CompositionRoot class must look like this: - -```csharp -public partial class CompositionRoot -{ - public void RegisterServices(IServiceRegistry serviceRegistry) - { - // Register services here - } -} -``` - -### Register all services in the project -You can also register all services in an project by adding the RegisterAll attribute to the assembly. This will register all implementations of the specified type. - -```csharp - -using DependencyInjection.SourceGenerator.Contracts.Attributes; - -[assembly: RegisterAll] - -namespace RootNamespace.Services; - -public interface IExampleService -{ - string GetExample(); -} - -public class ExampleService1 : IExampleService -{ - public string GetExample() - { - return "Example 1"; - } -} - -public class ExampleService2 : IExampleService -{ - public string GetExample() - { - return "Example 2"; - } -} - -``` - -this will generate the following code: - -```csharp - -public class CompositionRoot : ICompositionRoot -{ - public static void Compose(IServiceRegistry serviceRegistry) - { - serviceRegistry.Register(new PerContainerLifetime()); - serviceRegistry.Register(new PerContainerLifetime()); - } -} -``` - -## Lifetime -The lifetime is an enum with the following values: -- Transient -- Scoped -- Singleton \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Contracts/Attributes/RegistrationExtensionAttribute.cs b/DependencyInjection.SourceGenerator.Microsoft.Contracts/Attributes/RegistrationExtensionAttribute.cs deleted file mode 100644 index 2447dbf..0000000 --- a/DependencyInjection.SourceGenerator.Microsoft.Contracts/Attributes/RegistrationExtensionAttribute.cs +++ /dev/null @@ -1,7 +0,0 @@ - -namespace DependencyInjection.SourceGenerator.Microsoft.Contracts.Attributes; - -[AttributeUsage(AttributeTargets.Method)] -public class RegistrationExtensionAttribute : Attribute -{ -} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Contracts/DependencyInjection.SourceGenerator.Microsoft.Contracts.csproj b/DependencyInjection.SourceGenerator.Microsoft.Contracts/DependencyInjection.SourceGenerator.Microsoft.Contracts.csproj deleted file mode 100644 index 050ab86..0000000 --- a/DependencyInjection.SourceGenerator.Microsoft.Contracts/DependencyInjection.SourceGenerator.Microsoft.Contracts.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - net48;netstandard2.0;net6.0;net7.0;net8.0 - SourceGeneration - Contains types used to set up dependency injection registrations for Microsoft.Extensions.DependencyInjection - true - - - - - - - diff --git a/DependencyInjection.SourceGenerator.Microsoft.Demo/DependencyInjection.SourceGenerator.Microsoft.Demo.csproj b/DependencyInjection.SourceGenerator.Microsoft.Demo/DependencyInjection.SourceGenerator.Microsoft.Demo.csproj new file mode 100644 index 0000000..84e984d --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Demo/DependencyInjection.SourceGenerator.Microsoft.Demo.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + diff --git a/DependencyInjection.SourceGenerator.Microsoft.Demo/Program.cs b/DependencyInjection.SourceGenerator.Microsoft.Demo/Program.cs new file mode 100644 index 0000000..3751555 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Demo/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/DependencyInjection.SourceGenerator.Microsoft.Demo/TestService.cs b/DependencyInjection.SourceGenerator.Microsoft.Demo/TestService.cs new file mode 100644 index 0000000..5bb846e --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Demo/TestService.cs @@ -0,0 +1,7 @@ +namespace DependencyInjection.SourceGenerator.Microsoft.Demo; + +[global::Microsoft.Extensions.DependencyInjection.Register] +public class TestService +{ + +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/.editorconfig b/DependencyInjection.SourceGenerator.Microsoft.Tests/.editorconfig new file mode 100644 index 0000000..e69de29 diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/CSharpSourceGeneratorVerifier.cs b/DependencyInjection.SourceGenerator.Microsoft.Tests/CSharpSourceGeneratorVerifier.cs index 0b74aec..86a0026 100644 --- a/DependencyInjection.SourceGenerator.Microsoft.Tests/CSharpSourceGeneratorVerifier.cs +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/CSharpSourceGeneratorVerifier.cs @@ -7,7 +7,7 @@ namespace DependencyInjection.SourceGenerator.Microsoft.Tests; public static class CSharpSourceGeneratorVerifier - where TSourceGenerator : ISourceGenerator, new() + where TSourceGenerator : IIncrementalGenerator, new() { public class Test : CSharpSourceGeneratorTest { diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/DependencyInjection.SourceGenerator.Microsoft.Tests.csproj b/DependencyInjection.SourceGenerator.Microsoft.Tests/DependencyInjection.SourceGenerator.Microsoft.Tests.csproj index 7b42a4b..f69d0a3 100644 --- a/DependencyInjection.SourceGenerator.Microsoft.Tests/DependencyInjection.SourceGenerator.Microsoft.Tests.csproj +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/DependencyInjection.SourceGenerator.Microsoft.Tests.csproj @@ -10,34 +10,37 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + - + + + - + \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/DependencyInjectionRegistrationGeneratorTests.cs b/DependencyInjection.SourceGenerator.Microsoft.Tests/DependencyInjectionRegistrationGeneratorTests.cs index a022853..77307bc 100644 --- a/DependencyInjection.SourceGenerator.Microsoft.Tests/DependencyInjectionRegistrationGeneratorTests.cs +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/DependencyInjectionRegistrationGeneratorTests.cs @@ -1,71 +1,69 @@ using Microsoft.CodeAnalysis; using System.Collections.Immutable; -using Microsoft.CodeAnalysis.Text; -using System.Text; -using VerifyCS = DependencyInjection.SourceGenerator.Microsoft.Tests.CSharpSourceGeneratorVerifier; -using Microsoft.CodeAnalysis.Testing; -using NuGet.Frameworks; using FluentAssertions; using Microsoft.CodeAnalysis.CSharp; -using System.Diagnostics; using System.Reflection; +using System.Runtime.CompilerServices; +using global::Microsoft.Extensions.DependencyInjection; namespace DependencyInjection.SourceGenerator.Microsoft.Tests; public class DependencyInjectionRegistrationGeneratorTests { - private static readonly string _header = """ - // - #pragma warning disable - #nullable enable - namespace Microsoft.Extensions.DependencyInjection; - using global::Microsoft.Extensions.DependencyInjection; - - [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - - """; - - private readonly ImmutableArray _references = AppDomain.CurrentDomain - .GetAssemblies() - .Where(assembly => !assembly.IsDynamic) - .Select(assembly => assembly.Location) - .ToImmutableArray(); - - private async Task RunTestAsync(string code, string expectedResult) + private static async Task RunTestAsync(string code, [CallerMemberName] string methodName = "", bool validateCompilation = true) { - if (!NuGetFramework.Parse("net8.0").IsPackageBased) + List references = []; + + var assemblies = AppDomain.CurrentDomain + .GetAssemblies() + .Where(assembly => !assembly.IsDynamic) + .ToList(); + + var dependencyInjectionAssembly = typeof(ServiceLifetime).Assembly; + if (!assemblies.Contains(dependencyInjectionAssembly)) + assemblies.Add(dependencyInjectionAssembly); + + var scrutorAssembly = typeof(Scrutor.DecorationStrategy).Assembly; + if (!assemblies.Contains(scrutorAssembly)) + assemblies.Add(scrutorAssembly); + + foreach (var assemblyPath in assemblies) { - // The NuGet version provided at runtime does not recognize the 'net6.0' target framework - throw new NotSupportedException("The 'net8.0' target framework is not supported by this version of NuGet."); + references.Add(MetadataReference.CreateFromFile(assemblyPath.Location)); } - var net8 = new ReferenceAssemblies( - "net8.0", - new PackageIdentity( - "Microsoft.NETCore.App.Ref", - "8.0.0"), - Path.Combine("ref", "net8.0")); + var syntax = CSharpSyntaxTree.ParseText(code); + + var compilation = CSharpCompilation.Create( + "TestProject", + syntaxTrees: [syntax], + references: references, + options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var driver = CSharpGeneratorDriver.Create(new DependencyInjectionRegistrationGenerator()); - var tester = new VerifyCS.Test + driver.RunGeneratorsAndUpdateCompilation( + compilation, + out Compilation outputCompilation, + out ImmutableArray generatorDiagnostics); + + if (validateCompilation) + { + var diagnostics = outputCompilation.GetDiagnostics(); + diagnostics.Should().BeEmpty("because there should be no compilation errors after running the generator"); + } + + foreach (var syntaxTree in outputCompilation.SyntaxTrees) { - TestState = - { - Sources = { code }, - GeneratedSources = - { - (typeof(DependencyInjectionRegistrationGenerator), "ServiceCollectionExtensions.g.cs", - SourceText.From(expectedResult, Encoding.UTF8)) - } - }, - ReferenceAssemblies = net8 - }; - - tester.ReferenceAssemblies.AddAssemblies(_references); - tester.TestState.AdditionalReferences.Add(typeof(global::DependencyInjection.SourceGenerator.Contracts.Attributes.RegisterAttribute).Assembly); - tester.TestState.AdditionalReferences.Add(typeof(global::DependencyInjection.SourceGenerator.Microsoft.Contracts.Attributes.RegistrationExtensionAttribute).Assembly); - tester.TestState.AdditionalReferences.Add(typeof(global::Microsoft.Extensions.DependencyInjection.IServiceCollection).Assembly); - tester.TestState.AdditionalReferences.Add(typeof(global::Scrutor.RegistrationStrategy).Assembly); - await tester.RunAsync(); + if (syntaxTree.FilePath.EndsWith("ServiceRegistrations.g.cs") == false) + continue; + + var generatedSource = syntaxTree.ToString().Replace("\r\n", "\n"); + var settings = new VerifySettings(); + settings.UseDirectory("TestResults"); + settings.UseFileName(methodName + "_" + Path.GetFileNameWithoutExtension(syntaxTree.FilePath)); + await Verifier.Verify(generatedSource, settings); + } } [Theory] @@ -82,35 +80,77 @@ public void GetSafeMethodName(string assemblyName, string expectedMethodName) public async Task Register_DefaultValues() { var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; +using global::Microsoft.Extensions.DependencyInjection; + +namespace DependencyInjection.SourceGenerator.Microsoft.Demo; + +[Register] +public class Service : IService {} +public interface IService {} + +"""; + + await RunTestAsync(code); + } + + + [Fact] + public async Task Register_Record() + { + var code = """ +using global::Microsoft.Extensions.DependencyInjection; namespace DependencyInjection.SourceGenerator.Microsoft.Demo; [Register] +public sealed record Service; +"""; + + await RunTestAsync(code); + } + + [Fact] + public async Task Register_WithFactory() + { + var code = """ +using global::Microsoft.Extensions.DependencyInjection; + +namespace DependencyInjection.SourceGenerator.Microsoft.Demo; + +[Register(IncludeFactory = true)] public class Service : IService {} public interface IService {} """; - var expected = _header + """ -public static partial class ServiceCollectionExtensions + await RunTestAsync(code); + } + + [Fact] + public async Task Register_WithCollection() + { + var code = """ +using global::Microsoft.Extensions.DependencyInjection; + +namespace DependencyInjection.SourceGenerator.Microsoft.Demo; + +public class Test { - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + [Register] + public static void RegisterMethod(IServiceCollection services) { - services.AddTransient(); - return services; } } """; - await RunTestAsync(code, expected); + await RunTestAsync(code); } [Fact] public async Task Register_MultipleServices() { var code = """ -using DependencyInjection.SourceGenerator.Contracts.Attributes; +using global::Microsoft.Extensions.DependencyInjection; namespace DependencyInjection.SourceGenerator.Microsoft.Demo; @@ -122,26 +162,14 @@ public interface IService2 {} """; - var expected = _header + """ -public static partial class ServiceCollectionExtensions -{ - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.AddTransient(); - services.AddTransient(); - return services; - } -} -"""; - - await RunTestAsync(code, expected); + await RunTestAsync(code); } [Fact] - public void Register_UndefinedService() + public async Task Register_UndefinedService() { var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; +using global::Microsoft.Extensions.DependencyInjection; namespace DependencyInjection.SourceGenerator.Microsoft.Demo; @@ -158,85 +186,31 @@ public class Service3 : IService {} """; - var expected = _header + """ -public static partial class ServiceCollectionExtensions -{ - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - return services; - } -} -"""; - List references = []; - var assemblies = AppDomain.CurrentDomain.GetAssemblies(); - - foreach (Assembly assembly in assemblies) - { - if (!assembly.IsDynamic) - { - references.Add(MetadataReference.CreateFromFile(assembly.Location)); - } - } - - var syntax = CSharpSyntaxTree.ParseText(code); - - var compilation = CSharpCompilation.Create( - "TestProject", - syntaxTrees: [syntax], - references: references, - options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - - var driver = CSharpGeneratorDriver - .Create(new DependencyInjectionRegistrationGenerator()); - - driver.RunGeneratorsAndUpdateCompilation( - compilation, - out Compilation outputCompilation, - out ImmutableArray diagnostics); - - var generatedSource = outputCompilation.SyntaxTrees.Last().ToString(); - - generatedSource.Should().Be(expected); + await RunTestAsync(code, validateCompilation: false); } [Fact] public async Task Register_ScopedLifetime_And_ServiceName() { var code = """ - using DependencyInjection.SourceGenerator.Contracts.Attributes; - using DependencyInjection.SourceGenerator.Contracts.Enums; + using global::Microsoft.Extensions.DependencyInjection; namespace DependencyInjection.SourceGenerator.Microsoft.Demo; - [Register(Lifetime = Lifetime.Scoped, ServiceName = "Test")] + [Register(Lifetime = ServiceLifetime.Scoped, ServiceName = "Test")] public class Service : IService {} public interface IService {} """; - var expected = _header + """ - public static partial class ServiceCollectionExtensions - { - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.AddKeyedScoped("Test"); - return services; - } - } - """; - - await RunTestAsync(code, expected); + await RunTestAsync(code); } [Fact] public async Task Register_Specified_ServiceType() { var code = """ - using DependencyInjection.SourceGenerator.Contracts.Attributes; - using DependencyInjection.SourceGenerator.Contracts.Enums; + using global::Microsoft.Extensions.DependencyInjection; namespace DependencyInjection.SourceGenerator.Microsoft.Demo; @@ -246,26 +220,15 @@ public interface IService {} """; - var expected = _header + """ - public static partial class ServiceCollectionExtensions - { - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.AddTransient>(); - return services; - } - } - """; - - await RunTestAsync(code, expected); + await RunTestAsync(code); } [Fact] - public async Task Register_NoInteface_Or_BaseClass() + public async Task Register_NoInterface_Or_BaseClass() { var code = """ - using DependencyInjection.SourceGenerator.Contracts.Attributes; - using DependencyInjection.SourceGenerator.Contracts.Enums; + using global::Microsoft.Extensions.DependencyInjection; + namespace DependencyInjection.SourceGenerator.Microsoft.Demo; @@ -274,25 +237,14 @@ public class Service {} """; - var expected = _header + """ - public static partial class ServiceCollectionExtensions - { - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.AddTransient(); - return services; - } - } - """; - - await RunTestAsync(code, expected); + await RunTestAsync(code); } [Fact] public async Task Register_Specified_ServiceType_UsingGeneric() { var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; +using global::Microsoft.Extensions.DependencyInjection; namespace DependencyInjection.SourceGenerator.Microsoft.Demo; @@ -303,25 +255,14 @@ public interface IService2 {} """; - var expected = _header + """ -public static partial class ServiceCollectionExtensions -{ - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.AddTransient(); - return services; - } -} -"""; - - await RunTestAsync(code, expected); + await RunTestAsync(code); } [Fact] public async Task Decorate_DefaultValues() { var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; +using global::Microsoft.Extensions.DependencyInjection; namespace DependencyInjection.SourceGenerator.Microsoft.Demo; @@ -331,25 +272,14 @@ public interface IService {} """; - var expected = _header + """ -public static partial class ServiceCollectionExtensions -{ - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.Decorate(); - return services; - } -} -"""; - - await RunTestAsync(code, expected); + await RunTestAsync(code); } [Fact] public async Task Decorate_Specified_ServiceType() { var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; +using global::Microsoft.Extensions.DependencyInjection; namespace DependencyInjection.SourceGenerator.Microsoft.Demo; @@ -360,25 +290,14 @@ public interface IService2 {} """; - var expected = _header + """ -public static partial class ServiceCollectionExtensions -{ - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.Decorate(); - return services; - } -} -"""; - - await RunTestAsync(code, expected); + await RunTestAsync(code); } [Fact] public async Task Decorate_Specified_ServiceType_UsingGeneric() { var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; +using global::Microsoft.Extensions.DependencyInjection; namespace DependencyInjection.SourceGenerator.Microsoft.Demo; @@ -389,59 +308,14 @@ public interface IService2 {} """; - var expected = _header + """ -public static partial class ServiceCollectionExtensions -{ - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.Decorate(); - return services; - } -} -"""; - - await RunTestAsync(code, expected); - } - - [Fact] - public async Task Register_FromRegistrator() - { - var code = """ -using global::DependencyInjection.SourceGenerator.Microsoft.Contracts.Attributes; -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; - -namespace DependencyInjection.SourceGenerator.Microsoft.Demo; - -public class Registrator -{ - [RegistrationExtension] - internal static void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - } -} - -"""; - - var expected = _header + """ -public static partial class ServiceCollectionExtensions -{ - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - global::DependencyInjection.SourceGenerator.Microsoft.Demo.Registrator.Register(services); - return services; - } -} -"""; - - await RunTestAsync(code, expected); + await RunTestAsync(code); } - [Fact] public async Task RegisterAll_ByInterface() { var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; +using global::Microsoft.Extensions.DependencyInjection; [assembly: RegisterAll] [assembly: RegisterAll] @@ -458,21 +332,7 @@ public interface IServiceB {} """; - var expected = _header + """ -public static partial class ServiceCollectionExtensions -{ - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - return services; - } -} -"""; - - await RunTestAsync(code, expected); + await RunTestAsync(code); } @@ -480,10 +340,9 @@ public static partial class ServiceCollectionExtensions public async Task RegisterAll_SpecifyLifetime() { var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; -using global::DependencyInjection.SourceGenerator.Contracts.Enums; +using global::Microsoft.Extensions.DependencyInjection; -[assembly: RegisterAll(Lifetime = Lifetime.Singleton)] +[assembly: RegisterAll(Lifetime = ServiceLifetime.Singleton)] namespace DependencyInjection.SourceGenerator.Microsoft.Demo; @@ -493,26 +352,14 @@ public interface IService {} """; - var expected = _header + """ -public static partial class ServiceCollectionExtensions -{ - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.AddSingleton(); - services.AddSingleton(); - return services; - } -} -"""; - - await RunTestAsync(code, expected); + await RunTestAsync(code); } [Fact] public async Task RegisterAll_ByBaseType_WithServiceName() { var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; +using global::Microsoft.Extensions.DependencyInjection; [assembly: RegisterAll(IncludeServiceName = true)] @@ -524,26 +371,14 @@ public abstract class MyBase {} """; - var expected = _header + """ -public static partial class ServiceCollectionExtensions -{ - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.AddKeyedTransient("Service2"); - services.AddKeyedTransient("Service1"); - return services; - } -} -"""; - - await RunTestAsync(code, expected); + await RunTestAsync(code); } [Fact] public async Task RegisterAll_ByBaseType_WithoutServiceName() { var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; +using global::Microsoft.Extensions.DependencyInjection; [assembly: RegisterAll] @@ -555,26 +390,14 @@ public abstract class MyBase {} """; - var expected = _header + """ -public static partial class ServiceCollectionExtensions -{ - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.AddTransient(); - services.AddTransient(); - return services; - } -} -"""; - - await RunTestAsync(code, expected); + await RunTestAsync(code); } [Fact] public async Task RegisterAll_GenericInterfaceType() { var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; +using global::Microsoft.Extensions.DependencyInjection; using global::DependencyInjection.SourceGenerator.Microsoft.Demo; [assembly: RegisterAll(typeof(IService<>))] @@ -588,26 +411,14 @@ public class Service2 : IService {} """; - var expected = _header + """ -public static partial class ServiceCollectionExtensions -{ - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.AddTransient, global::DependencyInjection.SourceGenerator.Microsoft.Demo.Service2>(); - services.AddTransient, global::DependencyInjection.SourceGenerator.Microsoft.Demo.Service1>(); - return services; - } -} -"""; - - await RunTestAsync(code, expected); + await RunTestAsync(code); } [Fact] public async Task RegisterAll_GenericBaseType() { var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; +using global::Microsoft.Extensions.DependencyInjection; using global::DependencyInjection.SourceGenerator.Microsoft.Demo; [assembly: RegisterAll(typeof(Base<>))] @@ -620,19 +431,7 @@ public class Service2 : Base {} """; - var expected = _header + """ -public static partial class ServiceCollectionExtensions -{ - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.AddTransient, global::DependencyInjection.SourceGenerator.Microsoft.Demo.Service2>(); - services.AddTransient, global::DependencyInjection.SourceGenerator.Microsoft.Demo.Service1>(); - return services; - } -} -"""; - - await RunTestAsync(code, expected); + await RunTestAsync(code); } @@ -640,7 +439,7 @@ public static partial class ServiceCollectionExtensions public async Task RegisterAll_GenericBaseClassType() { var code = """ -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; +using global::Microsoft.Extensions.DependencyInjection; using global::DependencyInjection.SourceGenerator.Microsoft.Demo; [assembly: RegisterAll(typeof(BaseType<>))] @@ -653,17 +452,31 @@ public class Service : MyType {} """; - var expected = _header + """ -public static partial class ServiceCollectionExtensions + await RunTestAsync(code); + } + + [Fact] + public async Task RegisterMethod_DefaultValues() + { + var code = +""" +using global::Microsoft.Extensions.DependencyInjection; + +namespace DependencyInjection.SourceGenerator.Microsoft.Demo; + +public class Test { - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + [Register] + public static IService RegisterMethod(System.IServiceProvider services) { - services.AddTransient, global::DependencyInjection.SourceGenerator.Microsoft.Demo.Service>(); - return services; + return new Service(); } } +public class Service : IService {} +public interface IService {} + """; - await RunTestAsync(code, expected); + await RunTestAsync(code); } } \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Decorate_DefaultValues_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Decorate_DefaultValues_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..3a910e1 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Decorate_DefaultValues_ServiceRegistrations.g.verified.txt @@ -0,0 +1,15 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; +using Scrutor; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.Decorate(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Decorate_Specified_ServiceType_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Decorate_Specified_ServiceType_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..62bf78b --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Decorate_Specified_ServiceType_ServiceRegistrations.g.verified.txt @@ -0,0 +1,15 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; +using Scrutor; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.Decorate(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Decorate_Specified_ServiceType_UsingGeneric_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Decorate_Specified_ServiceType_UsingGeneric_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..62bf78b --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Decorate_Specified_ServiceType_UsingGeneric_ServiceRegistrations.g.verified.txt @@ -0,0 +1,15 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; +using Scrutor; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.Decorate(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_ByBaseType_WithServiceName_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_ByBaseType_WithServiceName_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..50dc639 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_ByBaseType_WithServiceName_ServiceRegistrations.g.verified.txt @@ -0,0 +1,15 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddKeyedTransient("Service1"); + services.AddKeyedTransient("Service2"); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_ByBaseType_WithoutServiceName_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_ByBaseType_WithoutServiceName_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..c9d3620 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_ByBaseType_WithoutServiceName_ServiceRegistrations.g.verified.txt @@ -0,0 +1,15 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_ByInterface_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_ByInterface_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..e5db511 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_ByInterface_ServiceRegistrations.g.verified.txt @@ -0,0 +1,17 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_GenericBaseClassType_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_GenericBaseClassType_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..044e01b --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_GenericBaseClassType_ServiceRegistrations.g.verified.txt @@ -0,0 +1,14 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient, global::DependencyInjection.SourceGenerator.Microsoft.Demo.Service>(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_GenericBaseType_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_GenericBaseType_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..4bd17cb --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_GenericBaseType_ServiceRegistrations.g.verified.txt @@ -0,0 +1,15 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient, global::DependencyInjection.SourceGenerator.Microsoft.Demo.Service1>(); + services.AddTransient, global::DependencyInjection.SourceGenerator.Microsoft.Demo.Service2>(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_GenericInterfaceType_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_GenericInterfaceType_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..bc1f439 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_GenericInterfaceType_ServiceRegistrations.g.verified.txt @@ -0,0 +1,15 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient, global::DependencyInjection.SourceGenerator.Microsoft.Demo.Service1>(); + services.AddTransient, global::DependencyInjection.SourceGenerator.Microsoft.Demo.Service2>(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_SpecifyLifetime_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_SpecifyLifetime_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..9e30e20 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterAll_SpecifyLifetime_ServiceRegistrations.g.verified.txt @@ -0,0 +1,15 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterMethod_DefaultValues_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterMethod_DefaultValues_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..e11b013 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/RegisterMethod_DefaultValues_ServiceRegistrations.g.verified.txt @@ -0,0 +1,14 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(global::DependencyInjection.SourceGenerator.Microsoft.Demo.Test.RegisterMethod); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_DefaultValues_Service.ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_DefaultValues_Service.ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..b272716 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_DefaultValues_Service.ServiceRegistrations.g.verified.txt @@ -0,0 +1,15 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +public static partial class ServiceCollectionExtensions +{ + public static partial global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_DefaultValues_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_DefaultValues_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..11efcdf --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_DefaultValues_ServiceRegistrations.g.verified.txt @@ -0,0 +1,14 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_MultipleServices_Service.ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_MultipleServices_Service.ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..5d62a57 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_MultipleServices_Service.ServiceRegistrations.g.verified.txt @@ -0,0 +1,16 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +public static partial class ServiceCollectionExtensions +{ + public static partial global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_MultipleServices_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_MultipleServices_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..3730c42 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_MultipleServices_ServiceRegistrations.g.verified.txt @@ -0,0 +1,15 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_NoInterface_Or_BaseClass_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_NoInterface_Or_BaseClass_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..e5bfac6 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_NoInterface_Or_BaseClass_ServiceRegistrations.g.verified.txt @@ -0,0 +1,14 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_Record_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_Record_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..e5bfac6 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_Record_ServiceRegistrations.g.verified.txt @@ -0,0 +1,14 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_ScopedLifetime_And_ServiceName_Service.ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_ScopedLifetime_And_ServiceName_Service.ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..4a043cb --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_ScopedLifetime_And_ServiceName_Service.ServiceRegistrations.g.verified.txt @@ -0,0 +1,15 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +public static partial class ServiceCollectionExtensions +{ + public static partial global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddKeyedScoped("Test"); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_ScopedLifetime_And_ServiceName_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_ScopedLifetime_And_ServiceName_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..18bdbfc --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_ScopedLifetime_And_ServiceName_ServiceRegistrations.g.verified.txt @@ -0,0 +1,14 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddKeyedScoped("Test"); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_Specified_ServiceType_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_Specified_ServiceType_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..00b96e8 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_Specified_ServiceType_ServiceRegistrations.g.verified.txt @@ -0,0 +1,14 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient>(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_Specified_ServiceType_UsingGeneric_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_Specified_ServiceType_UsingGeneric_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..2f1f7af --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_Specified_ServiceType_UsingGeneric_ServiceRegistrations.g.verified.txt @@ -0,0 +1,14 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_UndefinedService_Service1.ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_UndefinedService_Service1.ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..a95f328 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_UndefinedService_Service1.ServiceRegistrations.g.verified.txt @@ -0,0 +1,15 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +public static partial class ServiceCollectionExtensions +{ + public static partial global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_UndefinedService_Service2.ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_UndefinedService_Service2.ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..e267dbf --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_UndefinedService_Service2.ServiceRegistrations.g.verified.txt @@ -0,0 +1,15 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +public static partial class ServiceCollectionExtensions +{ + public static partial global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_UndefinedService_Service3.ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_UndefinedService_Service3.ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..b475c3b --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_UndefinedService_Service3.ServiceRegistrations.g.verified.txt @@ -0,0 +1,15 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +public static partial class ServiceCollectionExtensions +{ + public static partial global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_UndefinedService_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_UndefinedService_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..2633146 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_UndefinedService_ServiceRegistrations.g.verified.txt @@ -0,0 +1,16 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_WithCollection_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_WithCollection_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..7c0ebe7 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_WithCollection_ServiceRegistrations.g.verified.txt @@ -0,0 +1,14 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + global::DependencyInjection.SourceGenerator.Microsoft.Demo.Test.RegisterMethod(services); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_WithFactory_ServiceRegistrations.g.verified.txt b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_WithFactory_ServiceRegistrations.g.verified.txt new file mode 100644 index 0000000..e04e012 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft.Tests/TestResults/Register_WithFactory_ServiceRegistrations.g.verified.txt @@ -0,0 +1,15 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +public static partial class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + services.AddTransient>(factory => factory.GetRequiredService); + return services; + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft/AnalyzerReleases.Shipped.md b/DependencyInjection.SourceGenerator.Microsoft/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..b46f954 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft/AnalyzerReleases.Shipped.md @@ -0,0 +1,15 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +## Release 3.0.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +DI001 | DependencyInjection | Error | The method '{0}' used for registering services must be static +DI002 | DependencyInjection | Error | The method '{0}' used for registering services must be public or internal +DI003 | DependencyInjection | Error | The method '{0}' used for registering services must have exactly one parameter of type System.IServiceProvider +DI004 | DependencyInjection | Error | The method '{0}' used for registering services cannot return void +DI005 | DependencyInjection | Error | The method '{0}' used for registering services must have exactly one parameter of type System.IServiceProvider or Microsoft.Extensions.DependencyInjection.IServiceCollection + diff --git a/DependencyInjection.SourceGenerator.Microsoft/AnalyzerReleases.Unshipped.md b/DependencyInjection.SourceGenerator.Microsoft/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..b1b99aa --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft/AnalyzerReleases.Unshipped.md @@ -0,0 +1,3 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/DependencyInjection.SourceGenerator.Microsoft/Attributes/DecorateAttribute.cs b/DependencyInjection.SourceGenerator.Microsoft/Attributes/DecorateAttribute.cs new file mode 100644 index 0000000..0a5ee50 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft/Attributes/DecorateAttribute.cs @@ -0,0 +1,14 @@ +using System; + +namespace Microsoft.Extensions.DependencyInjection; + +[AttributeUsage(AttributeTargets.Class)] +internal class DecorateAttribute : Attribute +{ + public Type? ServiceType { get; set; } +} + +[AttributeUsage(AttributeTargets.Class)] +internal class DecorateAttribute : Attribute +{ +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft/Attributes/EmptyServiceCollectionExtensions.cs b/DependencyInjection.SourceGenerator.Microsoft/Attributes/EmptyServiceCollectionExtensions.cs new file mode 100644 index 0000000..3825078 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft/Attributes/EmptyServiceCollectionExtensions.cs @@ -0,0 +1,19 @@ +// +#pragma warning disable +#nullable enable +namespace Microsoft.Extensions.DependencyInjection; +using global::Microsoft.Extensions.DependencyInjection; + +[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +internal static partial class ServiceCollectionExtensions +{ + public static partial string AddTestProject(this string services) + { + return services; + } +} + +internal static partial class ServiceCollectionExtensions +{ + public static partial string AddTestProject(this string services); +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft/Attributes/RegisterAllAttribute.cs b/DependencyInjection.SourceGenerator.Microsoft/Attributes/RegisterAllAttribute.cs new file mode 100644 index 0000000..7186d36 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft/Attributes/RegisterAllAttribute.cs @@ -0,0 +1,27 @@ +using DependencyInjection.SourceGenerator.Microsoft.Enums; + +namespace Microsoft.Extensions.DependencyInjection; + +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +internal class RegisterAllAttribute(Type serviceType) : Attribute +{ + public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Transient; + public Type ServiceType { get; set; } = serviceType; + + public bool IncludeServiceName { get; set; } + public bool IncludeFactory { get; set; } + + public RegisterAllAttribute(Type serviceType, ServiceLifetime lifetime) : this(serviceType) + { + Lifetime = lifetime; + } +} + + +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] +internal class RegisterAllAttribute : Attribute +{ + public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Transient; + public bool IncludeServiceName { get; set; } + public bool IncludeFactory { get; set; } +} diff --git a/DependencyInjection.SourceGenerator.Microsoft/Attributes/RegisterAttribute.cs b/DependencyInjection.SourceGenerator.Microsoft/Attributes/RegisterAttribute.cs new file mode 100644 index 0000000..ec015e8 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft/Attributes/RegisterAttribute.cs @@ -0,0 +1,20 @@ +using DependencyInjection.SourceGenerator.Microsoft.Enums; + +namespace Microsoft.Extensions.DependencyInjection; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] +internal class RegisterAttribute : Attribute +{ + public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Transient; + public string? ServiceName { get; set; } + public bool IncludeFactory { get; set; } + public Type? ServiceType { get; set; } +} + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +internal class RegisterAttribute : Attribute +{ + public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Transient; + public string? ServiceName { get; set; } + public bool IncludeFactory { get; set; } +} diff --git a/DependencyInjection.SourceGenerator.Microsoft/DependencyInjection.SourceGenerator.Microsoft.csproj b/DependencyInjection.SourceGenerator.Microsoft/DependencyInjection.SourceGenerator.Microsoft.csproj index e1791b2..a824ed1 100644 --- a/DependencyInjection.SourceGenerator.Microsoft/DependencyInjection.SourceGenerator.Microsoft.csproj +++ b/DependencyInjection.SourceGenerator.Microsoft/DependencyInjection.SourceGenerator.Microsoft.csproj @@ -1,13 +1,21 @@  - - + library netstandard2.0 + enable + enable + Nullable + true + README.md + Frederik Tegnander + https://github.com/Frederik91/DependencyInjection.SourceGenerator + 12 true - readme.md + LICENSE + 3.0.0 - + SourceGenerator;Microsoft Generates dependency injection registration using source generation @@ -17,26 +25,36 @@ true + - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + - + - - - + - + + + + - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + diff --git a/DependencyInjection.SourceGenerator.Microsoft/DependencyInjectionRegistrationGenerator.cs b/DependencyInjection.SourceGenerator.Microsoft/DependencyInjectionRegistrationGenerator.cs index 74c5812..725ff27 100644 --- a/DependencyInjection.SourceGenerator.Microsoft/DependencyInjectionRegistrationGenerator.cs +++ b/DependencyInjection.SourceGenerator.Microsoft/DependencyInjectionRegistrationGenerator.cs @@ -3,125 +3,228 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; using System.Text; -using DependencyInjection.SourceGenerator.Contracts.Enums; -using DependencyInjection.SourceGenerator.Shared; -using DependencyInjection.SourceGenerator.Microsoft.Contracts.Attributes; +using DependencyInjection.SourceGenerator.Microsoft.Helpers; +using System.Collections.Immutable; +using DependencyInjection.SourceGenerator.Microsoft.Enums; +using Microsoft.Extensions.DependencyInjection; +using CodeGenHelpers.Internals; +using DependencyInjection.SourceGenerator.Microsoft.Diagnostics; namespace DependencyInjection.SourceGenerator.Microsoft; public record RegistrationExtension(string ClassFullName, string MethodName, List Errors); [Generator] -public class DependencyInjectionRegistrationGenerator : ISourceGenerator +public class DependencyInjectionRegistrationGenerator : IIncrementalGenerator { - public void Initialize(GeneratorInitializationContext context) + public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterForSyntaxNotifications(() => new ClassAttributeReceiver(additionalMethodAttributes: [nameof(RegistrationExtensionAttribute)])); + var declarations = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (s, _) => IsClassOrMethodWithAttributes(s), + transform: static (ctx, _) => GetSymbolToRegister(ctx)) + .Where(static m => m is not null); + + var compilationAndDeclarations = context.CompilationProvider.Combine(declarations.Collect()); + context.RegisterSourceOutput(compilationAndDeclarations, static (spc, source) => Execute(source.Left, source.Right, spc)); + context.RegisterPostInitializationOutput(static spc => + { + spc.AddSource("RegisterAttribute.g.cs", SourceText.From(AttributeSourceTexts.RegisterAttributeText, Encoding.UTF8)); + spc.AddSource("RegisterAllAttribute.g.cs", SourceText.From(AttributeSourceTexts.RegisterAllAttributeText, Encoding.UTF8)); + spc.AddSource("DecorateAttribute.g.cs", SourceText.From(AttributeSourceTexts.DecorateAttributeText, Encoding.UTF8)); + }); } - public void Execute(GeneratorExecutionContext context) + private static bool IsClassOrMethodWithAttributes(SyntaxNode node) { - var @namespace = "Microsoft.Extensions.DependencyInjection"; - var safeAssemblyName = EscapeAssemblyNameToMethodName(context.Compilation.AssemblyName); - var extensionName = "Add" + safeAssemblyName; + return node is TypeDeclarationSyntax typeDeclaration && typeDeclaration.AttributeLists.Count > 0 + || node is MethodDeclarationSyntax methodDeclaration && methodDeclaration.AttributeLists.Count > 0; + } - var classesToRegister = RegistrationCollector.GetTypes(context); - var registerAllTypes = RegistrationCollector.GetRegisterAllTypes(context); + private static ISymbol? GetSymbolToRegister(GeneratorSyntaxContext context) + { + if (context.Node is TypeDeclarationSyntax typeDeclaration) + { + var semanticModel = context.SemanticModel; + if (semanticModel.GetDeclaredSymbol(typeDeclaration) is not INamedTypeSymbol classSymbol) + return null; - var source = GenerateExtensionMethod(context, extensionName, @namespace, classesToRegister, registerAllTypes); - var sourceText = source.ToFullString(); - context.AddSource("ServiceCollectionExtensions.g.cs", SourceText.From(sourceText, Encoding.UTF8)); + foreach (var attribute in classSymbol.GetAttributes()) + { + if (attribute.AttributeClass?.Name is (nameof(RegisterAttribute)) or (nameof(DecorateAttribute))) + { + return classSymbol; + } + } + } + else if (context.Node is MethodDeclarationSyntax methodDeclaration) + { + var semanticModel = context.SemanticModel; + var methodSymbol = semanticModel.GetDeclaredSymbol(methodDeclaration) as IMethodSymbol; + if (methodSymbol is null) + return null; + + foreach (var attribute in methodSymbol.GetAttributes()) + { + if (attribute.AttributeClass?.Name == nameof(RegisterAttribute)) + { + return methodSymbol; + } + } + + } + + return null; } - public static string EscapeAssemblyNameToMethodName(string? assemblyName) + private static void Execute(Compilation compilation, ImmutableArray symbolsToRegister, SourceProductionContext context) { - if (string.IsNullOrWhiteSpace(assemblyName)) - return "Default"; + var @namespace = "Microsoft.Extensions.DependencyInjection"; + var safeAssemblyName = EscapeAssemblyNameToMethodName(compilation.AssemblyName); + var extensionName = "Add" + safeAssemblyName; - var sb = new StringBuilder(); - var ensureNextUpper = true; - foreach (var c in assemblyName!) + var bodyMembers = new List(); + + var includeScrutor = false; + + foreach (var symbol in symbolsToRegister) { - if (char.IsLetterOrDigit(c)) + if (symbol is INamedTypeSymbol classSymbol) { - var letter = c; - if (ensureNextUpper) + var hasDecorators = ProcessClassSymbol(classSymbol, bodyMembers); + if (hasDecorators) { - letter = char.ToUpperInvariant(c); - ensureNextUpper = false; + includeScrutor = true; } - sb.Append(letter); } - else + else if (symbol is IMethodSymbol methodSymbol) { - ensureNextUpper = true; - continue; + ProcessMethodSymbol(methodSymbol, bodyMembers, context); } } - return sb.ToString(); - } - private static string GetDefaultNamespace(GeneratorExecutionContext context) - { - var @namespace = context.Compilation.SyntaxTrees - .SelectMany(x => x.GetRoot().DescendantNodes()) - .OfType() - .Select(x => x.Name.ToString()) - .Min(); + RegisterAllHandler.Process(compilation, bodyMembers); - if (@namespace is not null) - return @namespace; + var source = GenerateExtensionMethod(extensionName, @namespace, bodyMembers, includeScrutor); + var sourceText = source.ToFullString(); + context.AddSource("ServiceRegistrations.g.cs", SourceText.From(sourceText, Encoding.UTF8)); + } - @namespace = context.Compilation.SyntaxTrees - .SelectMany(x => x.GetRoot().DescendantNodes()) - .OfType() - .Select(x => x.Name.ToString()) - .Min(); + private static bool ProcessClassSymbol(INamedTypeSymbol classSymbol, List bodyMembers) + { + var registrations = RegistrationMapper.CreateRegistration(classSymbol); + foreach (var registration in registrations) + { + var (registrationExpression, factoryExpression) = RegistrationMapper.CreateRegistrationSyntaxFromClass( + registration.ServiceType, + registration.ImplementationTypeName, + registration.Lifetime, + registration.ServiceName, + registration.IncludeFactory); + bodyMembers.Add(registrationExpression); + if (factoryExpression is not null) + { + bodyMembers.Add(factoryExpression); + } + } - if (@namespace is not null) - return @namespace; + var decorations = DecorationMapper.CreateDecoration(classSymbol); + var hasDecorators = false; + foreach (var decoration in decorations) + { + bodyMembers.Add(CreateDecorationSyntax(decoration.DecoratedTypeName, decoration.DecoratorTypeName)); + hasDecorators = true; + } - throw new NotSupportedException("Unable to calculate namespace"); + return hasDecorators; } - private static CompilationUnitSyntax GenerateExtensionMethod(GeneratorExecutionContext context, string extensionName, string @namespace, IEnumerable classesToRegister, IEnumerable additionalRegistrations) + private static void ProcessMethodSymbol(IMethodSymbol methodSymbol, List bodyMembers, SourceProductionContext context) { - var bodyMembers = new List(); + // Ensure the method is static + if (!methodSymbol.IsStatic) + { + MethodRegistrationDiagnostics.ReportMustBeStatic(methodSymbol, context); + return; + } + + // Ensure the method is public or internal + if (methodSymbol.DeclaredAccessibility is not Accessibility.Public and not Accessibility.Internal) + { + MethodRegistrationDiagnostics.ReportMustBePublicOrInternal(methodSymbol, context); + return; + } + + // Ensure the method has exactly one parameter of type System.IServiceProvider + if (methodSymbol.Parameters.Length != 1) + { + MethodRegistrationDiagnostics.ReportInvalidParameters(methodSymbol, context); + return; + } - foreach (var type in classesToRegister) + var methodParameter = methodSymbol.Parameters[0].Type.ToDisplayString(); + if (methodParameter == "System.IServiceProvider") { - var registrations = RegistrationMapper.CreateRegistration(type); + // Ensure the method does not return void + if (methodSymbol.ReturnsVoid) + { + MethodRegistrationDiagnostics.ReportCannotReturnVoid(methodSymbol, context); + return; + } + // Handle method registration + var registrations = RegistrationMapper.CreateRegistrationFromMethod(methodSymbol); foreach (var registration in registrations) { - bodyMembers.Add(CreateRegistrationSyntax(registration.ServiceType, registration.ImplementationTypeName, registration.Lifetime, registration.ServiceName)); + // Create the registration expression using type symbols + var registrationExpression = RegistrationMapper.CreateRegistrationSyntaxFromFactoryMethod(registration); + bodyMembers.Add(registrationExpression); } + } + else if (methodParameter == "Microsoft.Extensions.DependencyInjection.IServiceCollection") + { + var registration = RegistrationMapper.CreateCollectionRegistration(methodSymbol); + var extensionMethodRegistration = RegistrationMapper.CreateRegistrationSyntaxFromCollectionMethod(registration); + bodyMembers.Add(extensionMethodRegistration); + } + else + { + MethodRegistrationDiagnostics.ReportInvalidMethodParameter(methodSymbol, context); + } - var decoration = DecorationMapper.CreateDecoration(type); - if (decoration is not null) - bodyMembers.Add(CreateDecorationSyntax(decoration.DecoratedTypeName, decoration.DecoratorTypeName)); - var registrationExtensions = CreateRegistrationExtensions(type); + } + + public static string EscapeAssemblyNameToMethodName(string? assemblyName) + { + if (string.IsNullOrWhiteSpace(assemblyName)) + return "Default"; - foreach (var registrationExtension in registrationExtensions) + var sb = new StringBuilder(); + var ensureNextUpper = true; + foreach (var c in assemblyName!) + { + if (char.IsLetterOrDigit(c)) { - if (!registrationExtension.Errors.Any()) - { - bodyMembers.Add(CreateRegistrationExtensionSyntax(registrationExtension.ClassFullName, registrationExtension.MethodName)); - continue; - } - foreach (var error in registrationExtension.Errors) + var letter = c; + if (ensureNextUpper) { - context.ReportDiagnostic(error); + letter = char.ToUpperInvariant(c); + ensureNextUpper = false; } + sb.Append(letter); + } + else + { + ensureNextUpper = true; + continue; } } + return sb.ToString(); + } - foreach (var registration in additionalRegistrations) - { - bodyMembers.Add(CreateRegistrationSyntax(registration.ServiceType, registration.ImplementationTypeName, registration.Lifetime, registration.ServiceName)); - } - - var methodModifiers = SyntaxFactory.TokenList([SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword)]); + private static CompilationUnitSyntax GenerateExtensionMethod(string extensionName, string @namespace, List bodyMembers, bool includeScrutor) + { + var methodModifiers = SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword)); var serviceCollectionSyntax = SyntaxFactory.QualifiedName( SyntaxFactory.QualifiedName( @@ -146,14 +249,13 @@ private static CompilationUnitSyntax GenerateExtensionMethod(GeneratorExecutionC SyntaxFactory.Token(SyntaxKind.ThisKeyword))) .WithType(serviceCollectionSyntax)))); - var body = SyntaxFactory.Block(bodyMembers.ToArray()); var returnStatement = SyntaxFactory.ReturnStatement(SyntaxFactory.IdentifierName("services")); body = body.AddStatements(returnStatement); methodDeclaration = methodDeclaration.WithBody(body); - var classModifiers = SyntaxFactory.TokenList([SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword), SyntaxFactory.Token(SyntaxKind.PartialKeyword)]); + var classModifiers = SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword), SyntaxFactory.Token(SyntaxKind.PartialKeyword)); var classDeclaration = SyntaxFactory.ClassDeclaration("ServiceCollectionExtensions") .WithModifiers(classModifiers) .WithMembers(SyntaxFactory.SingletonList(methodDeclaration)); @@ -168,96 +270,13 @@ private static CompilationUnitSyntax GenerateExtensionMethod(GeneratorExecutionC SyntaxFactory.IdentifierName("Extensions")), SyntaxFactory.IdentifierName("DependencyInjection"))); - return Trivia.CreateCompilationUnitSyntax(classDeclaration, @namespace, [dependencyInjectionUsingDirective]); - } - - internal static List CreateRegistrationExtensions(INamedTypeSymbol type) - { - var registrations = new List(); - foreach (var member in type.GetMembers()) + var usingDirectives = new List { dependencyInjectionUsingDirective }; + if (includeScrutor) { - if (member is not IMethodSymbol method) - continue; - - var attribute = TypeHelper.GetAttributes(member.GetAttributes()); - if (!attribute.Any()) - continue; - - List errors = []; - if (method.DeclaredAccessibility is not Accessibility.Public and not Accessibility.Internal and not Accessibility.Friend) - { - var diagnostic = Diagnostic.Create( - new DiagnosticDescriptor( - "DIM0001", - "Invalid method accessor", - "Method {0} on type {1} must be public or internal", - "InvalidConfig", - DiagnosticSeverity.Error, - true), null, method.Name, type.Name); - errors.Add(diagnostic); - } - - if (!method.IsStatic) - { - var diagnostic = Diagnostic.Create( - new DiagnosticDescriptor( - "DIM0002", - "Method must be static", - "Method {0} on type {1} must be static", - "InvalidConfig", - DiagnosticSeverity.Error, - true), null, method.Name, type.Name); - errors.Add(diagnostic); - } - - if (method.Parameters.Length != 1) - { - var diagnostic = Diagnostic.Create( - new DiagnosticDescriptor( - "DIM0002", - "Invalid parameter count", - "Method {0} on type {1} must have exactly one parameter of type IServiceCollection", - "InvalidConfig", - DiagnosticSeverity.Error, - true), null, method.Name, type.Name); - errors.Add(diagnostic); - } - - var firstParameter = method.Parameters.FirstOrDefault(); - if (firstParameter is not null && TypeHelper.GetFullName(firstParameter.Type) != "global::Microsoft.Extensions.DependencyInjection.IServiceCollection") - { - var diagnostic = Diagnostic.Create( - new DiagnosticDescriptor( - "DIM0002", - "Invalid parameter type", - "Method {0} on type {1} must have input parameter of type IServiceCollection", - "InvalidConfig", - DiagnosticSeverity.Error, - true), null, method.Name, type.Name); - errors.Add(diagnostic); - } - - var registration = new RegistrationExtension(TypeHelper.GetFullName(type), method.Name, errors); - registrations.Add(registration); + usingDirectives.Add(SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName("Scrutor"))); } - return registrations; - } - private static ExpressionStatementSyntax CreateRegistrationExtensionSyntax(string className, string methodName) - { - var arguments = SyntaxFactory.ArgumentList( - SyntaxFactory.SingletonSeparatedList( - SyntaxFactory.Argument( - SyntaxFactory.IdentifierName("services")))); - - var expressionExpression = SyntaxFactory.InvocationExpression( - SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.IdentifierName(className), - SyntaxFactory.IdentifierName(methodName))) - .WithArgumentList(arguments); - - return SyntaxFactory.ExpressionStatement(expressionExpression); + return Trivia.CreateCompilationUnitSyntax(classDeclaration, @namespace, [.. usingDirectives]); } private static ExpressionStatementSyntax CreateDecorationSyntax(string decoratedTypeName, string decoratorTypeName) @@ -286,69 +305,5 @@ private static ExpressionStatementSyntax CreateDecorationSyntax(string decorated return SyntaxFactory.ExpressionStatement(expression); } - private static ExpressionStatementSyntax CreateRegisterServicesCall() - { - return SyntaxFactory.ExpressionStatement( - SyntaxFactory.InvocationExpression( - SyntaxFactory.IdentifierName("RegisterServices")) - .WithArgumentList( - SyntaxFactory.ArgumentList( - SyntaxFactory.SingletonSeparatedList( - SyntaxFactory.Argument( - SyntaxFactory.IdentifierName("serviceRegistry")))))); - } - - private static ExpressionStatementSyntax CreateRegistrationSyntax(string? serviceType, string implementation, Lifetime lifetime, string? serviceName) - { - var keyed = serviceName is null ? string.Empty : "Keyed"; - var lifetimeName = lifetime switch - { - Lifetime.Singleton => $"Singleton", - Lifetime.Scoped => "Scoped", - Lifetime.Transient => "Transient", - _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) - }; - var methodName = $"Add{keyed}{lifetimeName}"; - - SyntaxNodeOrToken[] tokens; - if (serviceType is null) - { - tokens = [SyntaxFactory.IdentifierName(implementation)]; - } - else - { - tokens = - [ - SyntaxFactory.IdentifierName(serviceType), - SyntaxFactory.Token(SyntaxKind.CommaToken), - SyntaxFactory.IdentifierName(implementation) - ]; - } - - var accessExpression = SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.IdentifierName("services"), - SyntaxFactory.GenericName( - SyntaxFactory.Identifier(methodName)) - .WithTypeArgumentList( - SyntaxFactory.TypeArgumentList( - SyntaxFactory.SeparatedList(tokens)))); - var argumentList = SyntaxFactory.ArgumentList(); - if (serviceName is not null) - { - argumentList = SyntaxFactory.ArgumentList( - SyntaxFactory.SingletonSeparatedList( - SyntaxFactory.Argument( - SyntaxFactory.LiteralExpression( - SyntaxKind.StringLiteralExpression, - SyntaxFactory.Literal(serviceName))))); - - } - - var expression = SyntaxFactory.InvocationExpression(accessExpression) - .WithArgumentList(argumentList); - - return SyntaxFactory.ExpressionStatement(expression); - } } diff --git a/DependencyInjection.SourceGenerator.Microsoft/DependencyInjectionRegistrationGenerator.csproj b/DependencyInjection.SourceGenerator.Microsoft/DependencyInjectionRegistrationGenerator.csproj new file mode 100644 index 0000000..7d1adb2 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft/DependencyInjectionRegistrationGenerator.csproj @@ -0,0 +1,29 @@ + + + + + netstandard2.0 + latest + enable + false + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft/Diagnostics/MethodRegistrationDiagnostics.cs b/DependencyInjection.SourceGenerator.Microsoft/Diagnostics/MethodRegistrationDiagnostics.cs new file mode 100644 index 0000000..9a472ed --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft/Diagnostics/MethodRegistrationDiagnostics.cs @@ -0,0 +1,76 @@ +using Microsoft.CodeAnalysis; + +namespace DependencyInjection.SourceGenerator.Microsoft.Diagnostics; + +public static class MethodRegistrationDiagnostics +{ + private static readonly DiagnosticDescriptor MustBeStatic = new( + "DI001", + "Method Registration", + "Register method '{0}' must be static", + "DependencyInjection", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor MustBePublicOrInternal = new( + "DI002", + "Method Registration", + "Register method '{0}' must be public or internal", + "DependencyInjection", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor InvalidParameters = new( + "DI003", + "Method Registration", + "Register method '{0}' must have exactly one parameter of type System.IServiceProvider", + "DependencyInjection", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor CannotReturnVoid = new( + "DI004", + "Method Registration", + "Register method '{0}' cannot return void", + "DependencyInjection", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + private static readonly DiagnosticDescriptor MustHaveIServiceCollectionParameter = new( + "DI005", + "Method Registration", + "Register method '{0}' must have exactly one parameter of type System.IServiceProvider or Microsoft.Extensions.DependencyInjection.IServiceCollection", + "DependencyInjection", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static void ReportMustBeStatic(IMethodSymbol methodSymbol, SourceProductionContext context) + { + var diagnostic = Diagnostic.Create(MustBeStatic, Location.None, methodSymbol.Name); + context.ReportDiagnostic(diagnostic); + } + + public static void ReportMustBePublicOrInternal(IMethodSymbol methodSymbol, SourceProductionContext context) + { + var diagnostic = Diagnostic.Create(MustBePublicOrInternal, Location.None, methodSymbol.Name); + context.ReportDiagnostic(diagnostic); + } + + public static void ReportInvalidParameters(IMethodSymbol methodSymbol, SourceProductionContext context) + { + var diagnostic = Diagnostic.Create(InvalidParameters, Location.None, methodSymbol.Name); + context.ReportDiagnostic(diagnostic); + } + + public static void ReportCannotReturnVoid(IMethodSymbol methodSymbol, SourceProductionContext context) + { + var diagnostic = Diagnostic.Create(CannotReturnVoid, Location.None, methodSymbol.Name); + context.ReportDiagnostic(diagnostic); + } + + public static void ReportInvalidMethodParameter(IMethodSymbol methodSymbol, SourceProductionContext context) + { + var diagnostic = Diagnostic.Create(MustHaveIServiceCollectionParameter, Location.None, methodSymbol.Name); + context.ReportDiagnostic(diagnostic); + } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft/Enums/ServiceLifetime.cs b/DependencyInjection.SourceGenerator.Microsoft/Enums/ServiceLifetime.cs new file mode 100644 index 0000000..d61f585 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft/Enums/ServiceLifetime.cs @@ -0,0 +1,8 @@ +namespace DependencyInjection.SourceGenerator.Microsoft.Enums; + +public enum ServiceLifetime +{ + Singleton, + Scoped, + Transient +} diff --git a/DependencyInjection.SourceGenerator.Microsoft/Helpers/AttributeSourceTexts.cs b/DependencyInjection.SourceGenerator.Microsoft/Helpers/AttributeSourceTexts.cs new file mode 100644 index 0000000..26dd8e4 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft/Helpers/AttributeSourceTexts.cs @@ -0,0 +1,95 @@ +namespace DependencyInjection.SourceGenerator.Microsoft.Helpers; + +public static class AttributeSourceTexts +{ + public static string CreateDefaultServiceRegistrationsClassText(string assemblyName) => + $$""" +#nullable enable +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + public static partial class ServiceCollectionExtensions + { + public static partial global::Microsoft.Extensions.DependencyInjection.IServiceCollection Add{{assemblyName}}(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + return services; + } + } + + public static partial class ServiceCollectionExtensions + { + public static partial global::Microsoft.Extensions.DependencyInjection.IServiceCollection Add{{assemblyName}}(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services); + } +} +"""; + + public const string RegisterAttributeText = @" +#nullable enable +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, AllowMultiple = true)] + internal sealed class RegisterAttribute : global::System.Attribute + { + public global::Microsoft.Extensions.DependencyInjection.ServiceLifetime Lifetime { get; set; } = global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Transient; + public string? ServiceName { get; set; } + public bool IncludeFactory { get; set; } + public global::System.Type? ServiceType { get; set; } + } + + [global::System.AttributeUsage(global::System.AttributeTargets.Class | global::System.AttributeTargets.Method, AllowMultiple = true)] + internal sealed class RegisterAttribute : global::System.Attribute + { + public global::Microsoft.Extensions.DependencyInjection.ServiceLifetime Lifetime { get; set; } = global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Transient; + public string? ServiceName { get; set; } + public bool IncludeFactory { get; set; } + } +}"; + + public const string RegisterAllAttributeText = @" +#nullable enable +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.AttributeUsage(global::System.AttributeTargets.Assembly, AllowMultiple = true)] + internal sealed class RegisterAllAttribute : global::System.Attribute + { + public global::Microsoft.Extensions.DependencyInjection.ServiceLifetime Lifetime { get; set; } = global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Transient; + public global::System.Type ServiceType { get; set; } + public bool IncludeServiceName { get; set; } + public bool IncludeFactory { get; set; } + + public RegisterAllAttribute(global::System.Type serviceType) + { + ServiceType = serviceType; + } + + public RegisterAllAttribute(global::System.Type serviceType, global::Microsoft.Extensions.DependencyInjection.ServiceLifetime lifetime) : this(serviceType) + { + Lifetime = lifetime; + } + } + + [global::System.AttributeUsage(global::System.AttributeTargets.Assembly, AllowMultiple = true)] + internal sealed class RegisterAllAttribute : global::System.Attribute + { + public global::Microsoft.Extensions.DependencyInjection.ServiceLifetime Lifetime { get; set; } = global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Transient; + public bool IncludeServiceName { get; set; } + public bool IncludeFactory { get; set; } + } +}"; + + public const string DecorateAttributeText = @" +#nullable enable +namespace Microsoft.Extensions.DependencyInjection +{ + [global::System.AttributeUsage(global::System.AttributeTargets.Class)] + internal sealed class DecorateAttribute : global::System.Attribute + { + public global::System.Type? ServiceType { get; set; } + } + + [global::System.AttributeUsage(global::System.AttributeTargets.Class)] + internal sealed class DecorateAttribute : global::System.Attribute + { + } +}"; +} diff --git a/DependencyInjection.SourceGenerator.Shared/Decoration.cs b/DependencyInjection.SourceGenerator.Microsoft/Helpers/Decoration.cs similarity index 54% rename from DependencyInjection.SourceGenerator.Shared/Decoration.cs rename to DependencyInjection.SourceGenerator.Microsoft/Helpers/Decoration.cs index 327c6cb..78e834a 100644 --- a/DependencyInjection.SourceGenerator.Shared/Decoration.cs +++ b/DependencyInjection.SourceGenerator.Microsoft/Helpers/Decoration.cs @@ -1,7 +1,4 @@ -using DependencyInjection.SourceGenerator.Contracts.Enums; -using System; - -namespace DependencyInjection.SourceGenerator.Shared; +namespace DependencyInjection.SourceGenerator.Microsoft.Helpers; internal sealed class Decoration { diff --git a/DependencyInjection.SourceGenerator.Microsoft/Helpers/DecorationMapper.cs b/DependencyInjection.SourceGenerator.Microsoft/Helpers/DecorationMapper.cs new file mode 100644 index 0000000..982950a --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft/Helpers/DecorationMapper.cs @@ -0,0 +1,32 @@ +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; + +namespace DependencyInjection.SourceGenerator.Microsoft.Helpers; +internal static class DecorationMapper +{ + internal static List CreateDecoration(INamedTypeSymbol type) + { + var attributes = TypeHelper.GetClassAttributes(type); + + if (attributes is null || attributes.Count == 0) + return []; + + var result = new List(); + foreach (var attribute in attributes) + { + var serviceType = TypeHelper.GetServiceType(type, attribute); + if (serviceType is null) + return []; + + var decoration = new Decoration + { + DecoratorTypeName = TypeHelper.GetFullName(type), + DecoratedTypeName = serviceType.Name + }; + + result.Add(decoration); + } + + return result; + } +} diff --git a/DependencyInjection.SourceGenerator.Microsoft/Helpers/ImplementationLookup.cs b/DependencyInjection.SourceGenerator.Microsoft/Helpers/ImplementationLookup.cs new file mode 100644 index 0000000..1f3b901 --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft/Helpers/ImplementationLookup.cs @@ -0,0 +1,71 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace DependencyInjection.SourceGenerator.Microsoft.Helpers; + +public static class ImplementationLookup +{ + public static IEnumerable<(INamedTypeSymbol implmentingType, INamedTypeSymbol serviceType)> GetImplementations(Compilation compilation, INamedTypeSymbol serviceTypeSymbol) + { + foreach (var syntaxTree in compilation.SyntaxTrees) + { + var semanticModel = compilation.GetSemanticModel(syntaxTree); + var root = syntaxTree.GetRoot(); + var typeDeclarations = root.DescendantNodes().OfType(); + + foreach (var typeDeclaration in typeDeclarations) + { + var implementingType = semanticModel.GetDeclaredSymbol(typeDeclaration) as INamedTypeSymbol; + if (implementingType is null) + continue; + + if (implementingType.IsAbstract) + continue; + + if (ImplementsInterface(implementingType, serviceTypeSymbol, out var serviceTypeFromInterface)) + { + yield return (implementingType, serviceTypeFromInterface); + } + + if (IsDerivedFrom(implementingType, serviceTypeSymbol, out var serviceTypeFromBase)) + { + yield return (implementingType, serviceTypeFromBase); + continue; + } + } + } + } + + private static bool IsDerivedFrom(INamedTypeSymbol implementingType, INamedTypeSymbol serviceType, out INamedTypeSymbol implementedServiceType) + { + implementedServiceType = serviceType; + var currentBaseType = implementingType.BaseType; + while (currentBaseType != null) + { + if (SymbolEqualityComparer.Default.Equals(currentBaseType.OriginalDefinition, serviceType.OriginalDefinition)) + { + implementedServiceType = currentBaseType; + return true; + } + currentBaseType = currentBaseType.BaseType; + } + return false; + } + + private static bool ImplementsInterface(INamedTypeSymbol implementingType, INamedTypeSymbol serviceType, out INamedTypeSymbol implementedServiceType) + { + implementedServiceType = serviceType; + + foreach (var iface in implementingType.AllInterfaces) + { + if (iface.OriginalDefinition.Equals(serviceType.OriginalDefinition, SymbolEqualityComparer.Default)) + { + implementedServiceType = iface; + return true; + } + } + return false; + } +} diff --git a/DependencyInjection.SourceGenerator.Microsoft/Helpers/RegisterAllHandler.cs b/DependencyInjection.SourceGenerator.Microsoft/Helpers/RegisterAllHandler.cs new file mode 100644 index 0000000..9805abb --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft/Helpers/RegisterAllHandler.cs @@ -0,0 +1,38 @@ +using System; +using CodeGenHelpers.Internals; +using DependencyInjection.SourceGenerator.Microsoft.Enums; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Generic; +using System.Linq; + +namespace DependencyInjection.SourceGenerator.Microsoft.Helpers; + +public static class RegisterAllHandler +{ + public static void Process(Compilation compilation, List bodyMembers) + { + var registerAllAttributes = compilation.Assembly.GetAttributes().Where(a => a.AttributeClass?.Name == nameof(RegisterAllAttribute)).ToArray(); + + foreach (var attribute in registerAllAttributes) + { + var serviceType = TypeHelper.GetServiceTypeFromAttribute(attribute); + if (serviceType is null) + continue; + + var lifetime = TypeHelper.GetLifetimeFromAttribute(attribute) ?? ServiceLifetime.Transient; + var includeServiceName = TypeHelper.GetAttributeValue(attribute, nameof(RegisterAllAttribute.IncludeServiceName)) as bool? ?? false; + var includeFactory = TypeHelper.GetAttributeValue(attribute, nameof(RegisterAllAttribute.IncludeFactory)) as bool? ?? false; + var implementations = ImplementationLookup.GetImplementations(compilation, serviceType); + foreach (var (implementationType, actualServiceType) in implementations) + { + var serviceName = includeServiceName ? implementationType.Name : null; + var (registration, factory) = RegistrationMapper.CreateRegistrationSyntaxFromClass(actualServiceType.ToDisplayString(), implementationType.ToDisplayString(), lifetime, serviceName, includeFactory); + bodyMembers.Add(registration); + if (factory is not null) + bodyMembers.Add(factory); + } + } + } +} diff --git a/DependencyInjection.SourceGenerator.Microsoft/Helpers/Registration.cs b/DependencyInjection.SourceGenerator.Microsoft/Helpers/Registration.cs new file mode 100644 index 0000000..32d215d --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft/Helpers/Registration.cs @@ -0,0 +1,27 @@ +using DependencyInjection.SourceGenerator.Microsoft.Enums; + +namespace DependencyInjection.SourceGenerator.Microsoft.Helpers; + +internal sealed class ClassRegistration +{ + public required string? ServiceType { get; init; } + public required string? ServiceName { get; init; } + public required bool IncludeFactory { get; set; } + public required ServiceLifetime Lifetime { get; init; } + public required string ImplementationTypeName { get; init; } +} + +internal sealed class MethodFactoryRegistration +{ + public required string ServiceType { get; init; } + public required string? ServiceName { get; init; } + public required ServiceLifetime Lifetime { get; init; } + public required string MethodClassName { get; init; } + public required string MethodName { get; init; } +} + +internal sealed class MethodCollectionRegistration +{ + public required string MethodClassName { get; init; } + public required string MethodName { get; init; } +} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft/Helpers/RegistrationMapper.cs b/DependencyInjection.SourceGenerator.Microsoft/Helpers/RegistrationMapper.cs new file mode 100644 index 0000000..6d6705f --- /dev/null +++ b/DependencyInjection.SourceGenerator.Microsoft/Helpers/RegistrationMapper.cs @@ -0,0 +1,253 @@ +using DependencyInjection.SourceGenerator.Microsoft.Enums; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Extensions.DependencyInjection; + +namespace DependencyInjection.SourceGenerator.Microsoft.Helpers; +internal static class RegistrationMapper +{ + internal static List CreateRegistration(INamedTypeSymbol type) + { + var attributes = TypeHelper.GetClassAttributes(type); + + if (!attributes.Any()) + return []; + + var result = new List(); + foreach (var attribute in attributes) + { + + var serviceType = TypeHelper.GetServiceType(type, attribute); + if (serviceType is null) + continue; + + var lifetime = TypeHelper.GetLifetimeFromAttribute(attribute) ?? ServiceLifetime.Transient; + + var serviceNameArgument = TypeHelper.GetAttributeValue(attribute, nameof(RegisterAttribute.ServiceName)); + + var includeFactory = TypeHelper.GetAttributeValue(attribute, nameof(RegisterAttribute.IncludeFactory)) as bool? ?? false; + + // Get the value of the property + var serviceName = serviceNameArgument?.ToString(); + + var implementationTypeName = TypeHelper.GetFullName(type); + + if (TypeHelper.IsSameType(type, serviceType.Type)) + { + implementationTypeName = serviceType.Name; + serviceType = null; + } + + var registration = new ClassRegistration + { + ImplementationTypeName = implementationTypeName, + Lifetime = lifetime, + IncludeFactory = includeFactory, + ServiceName = serviceName, + ServiceType = serviceType?.Name + }; + result.Add(registration); + } + + return result; + } + + internal static List CreateRegistrationFromMethod(IMethodSymbol methodSymbol) + { + var registrations = new List(); + + var attributes = TypeHelper.GetMethodAttributes(methodSymbol); + foreach (var attribute in attributes) + { + var serviceTypeSymbol = TypeHelper.GetServiceTypeFromMethod(methodSymbol, attribute); + var serviceType = serviceTypeSymbol?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) ?? methodSymbol.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + var lifetime = TypeHelper.GetLifetimeFromAttribute(attribute) ?? ServiceLifetime.Transient; + var serviceName = TypeHelper.GetAttributeValue(attribute, "ServiceName") as string; + + registrations.Add(new MethodFactoryRegistration + { + ServiceType = serviceType, + ServiceName = serviceName, + Lifetime = lifetime, + MethodClassName = methodSymbol.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + MethodName = methodSymbol.Name + }); + } + + return registrations; + } + + public static (ExpressionStatementSyntax registrationExpression, ExpressionStatementSyntax? factoryExpression) CreateRegistrationSyntaxFromClass(string? serviceType, string implementation, ServiceLifetime lifetime, string? serviceName, bool includeFactory) + { + if (serviceType is not null) + { + serviceType = PrefixGlobalIfNotPresent(serviceType); + } + implementation = PrefixGlobalIfNotPresent(implementation); + var keyed = serviceName is null ? string.Empty : "Keyed"; + var methodName = $"Add{keyed}{lifetime}"; + + SyntaxNodeOrToken[] tokens; + if (serviceType is null) + { + tokens = [SyntaxFactory.IdentifierName(implementation)]; + } + else + { + tokens = + [ + SyntaxFactory.IdentifierName(serviceType), + SyntaxFactory.Token(SyntaxKind.CommaToken), + SyntaxFactory.IdentifierName(implementation) + ]; + } + + var accessExpression = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("services"), + SyntaxFactory.GenericName( + SyntaxFactory.Identifier(methodName)) + .WithTypeArgumentList( + SyntaxFactory.TypeArgumentList( + SyntaxFactory.SeparatedList(tokens)))); + + var argumentList = SyntaxFactory.ArgumentList(); + if (serviceName is not null) + { + argumentList = SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument( + SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(serviceName))))); + + } + + var expression = SyntaxFactory.InvocationExpression(accessExpression) + .WithArgumentList(argumentList); + + ExpressionStatementSyntax? factoryExpression = null; + if (includeFactory) + factoryExpression = CreateFactorySyntax(serviceType, implementation, lifetime, serviceName); + return (SyntaxFactory.ExpressionStatement(expression), factoryExpression); + } + + private static ExpressionStatementSyntax? CreateFactorySyntax(string? serviceType, string implementation, ServiceLifetime lifetime, string? serviceName) + { + var factoryMethodName = $"Add{(serviceName is null ? string.Empty : "Keyed")}{lifetime}"; + + SyntaxNodeOrToken[] tokens; + if (serviceType is null) + { + tokens = [SyntaxFactory.IdentifierName($"global::System.Func<{implementation}>")]; + } + else + { + tokens = + [ + SyntaxFactory.IdentifierName($"global::System.Func<{serviceType}>") + ]; + } + + var accessExpression = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("services"), + SyntaxFactory.GenericName( + SyntaxFactory.Identifier(factoryMethodName)) + .WithTypeArgumentList( + SyntaxFactory.TypeArgumentList( + SyntaxFactory.SeparatedList(tokens)))); + + var argumentList = SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument( + SyntaxFactory.SimpleLambdaExpression( + SyntaxFactory.Parameter(SyntaxFactory.Identifier("factory")), + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("factory"), + SyntaxFactory.GenericName( + SyntaxFactory.Identifier("GetRequiredService")) + .WithTypeArgumentList( + SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.IdentifierName(serviceType ?? implementation))))))))); + + if (serviceName is not null) + { + argumentList = argumentList.AddArguments( + SyntaxFactory.Argument( + SyntaxFactory.LiteralExpression( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal(serviceName)))); + } + + var expression = SyntaxFactory.InvocationExpression(accessExpression) + .WithArgumentList(argumentList); + + return SyntaxFactory.ExpressionStatement(expression); + } + + private static string PrefixGlobalIfNotPresent(string serviceType) + { + if (serviceType.StartsWith("global::")) + { + return serviceType; + } + return $"global::{serviceType}"; + } + + internal static ExpressionStatementSyntax CreateRegistrationSyntaxFromFactoryMethod(MethodFactoryRegistration registration) + { + var keyed = registration.ServiceName is null ? string.Empty : "Keyed"; + var methodName = $"Add{keyed}{registration.Lifetime}"; + return SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("services"), + SyntaxFactory.GenericName( + SyntaxFactory.Identifier(methodName)) + .WithTypeArgumentList( + SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.IdentifierName(registration.ServiceType)))))) + .WithArgumentList( + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(registration.MethodClassName), + SyntaxFactory.IdentifierName(registration.MethodName))))))) + .NormalizeWhitespace(); + } + + internal static ExpressionStatementSyntax CreateRegistrationSyntaxFromCollectionMethod(MethodCollectionRegistration registration) + { + return SyntaxFactory.ExpressionStatement( + SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName(registration.MethodClassName), + SyntaxFactory.IdentifierName(registration.MethodName))) + .WithArgumentList( + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.Argument( + SyntaxFactory.IdentifierName("services")))))) + .NormalizeWhitespace(); + } + + internal static MethodCollectionRegistration CreateCollectionRegistration(IMethodSymbol methodSymbol) + { + var registration = new MethodCollectionRegistration + { + MethodClassName = methodSymbol.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + MethodName = methodSymbol.Name + }; + + return registration; + } +} diff --git a/DependencyInjection.SourceGenerator.Shared/Trivia.cs b/DependencyInjection.SourceGenerator.Microsoft/Helpers/Trivia.cs similarity index 91% rename from DependencyInjection.SourceGenerator.Shared/Trivia.cs rename to DependencyInjection.SourceGenerator.Microsoft/Helpers/Trivia.cs index ed1824e..12491c7 100644 --- a/DependencyInjection.SourceGenerator.Shared/Trivia.cs +++ b/DependencyInjection.SourceGenerator.Microsoft/Helpers/Trivia.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Text; -namespace DependencyInjection.SourceGenerator.Shared; +namespace DependencyInjection.SourceGenerator.Microsoft.Helpers; internal static class Trivia { internal static AttributeListSyntax CreateExcludeFromCodeCoverage() @@ -46,8 +46,8 @@ internal static SyntaxToken CreateTrivia() internal static CompilationUnitSyntax CreateCompilationUnitSyntax(ClassDeclarationSyntax classDeclaration, string @namespace, UsingDirectiveSyntax[]? usings = null) { - var excludeFromCodeCoverageSyntax = CreateExcludeFromCodeCoverage(); - classDeclaration = classDeclaration.AddAttributeLists(excludeFromCodeCoverageSyntax); + // var excludeFromCodeCoverageSyntax = CreateExcludeFromCodeCoverage(); + // classDeclaration = classDeclaration.AddAttributeLists(excludeFromCodeCoverageSyntax); var trivia = Trivia.CreateTrivia(); var namespaceDeclaration = SyntaxFactory.FileScopedNamespaceDeclaration(SyntaxFactory.IdentifierName(@namespace)) diff --git a/DependencyInjection.SourceGenerator.Shared/TypeHelper.cs b/DependencyInjection.SourceGenerator.Microsoft/Helpers/TypeHelper.cs similarity index 81% rename from DependencyInjection.SourceGenerator.Shared/TypeHelper.cs rename to DependencyInjection.SourceGenerator.Microsoft/Helpers/TypeHelper.cs index f7d5bcb..d0d8a79 100644 --- a/DependencyInjection.SourceGenerator.Shared/TypeHelper.cs +++ b/DependencyInjection.SourceGenerator.Microsoft/Helpers/TypeHelper.cs @@ -1,12 +1,8 @@ -using DependencyInjection.SourceGenerator.Contracts.Attributes; -using DependencyInjection.SourceGenerator.Contracts.Enums; +using DependencyInjection.SourceGenerator.Microsoft.Enums; using Microsoft.CodeAnalysis; -using System; -using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; -namespace DependencyInjection.SourceGenerator.Shared; +namespace DependencyInjection.SourceGenerator.Microsoft.Helpers; internal static class TypeHelper { private static readonly SymbolDisplayFormat _displayFormat = SymbolDisplayFormat.FullyQualifiedFormat; @@ -18,6 +14,12 @@ internal static List GetClassAttributes(INamedTypeSym return GetAttributes(attributes); } + internal static List GetMethodAttributes(IMethodSymbol methodSymbol) where TAttribute : Attribute + { + var attributes = methodSymbol.GetAttributes(); + return GetAttributes(attributes); + } + internal static List GetAttributes(ImmutableArray attributes) where TAttribute : Attribute { var result = new List(); @@ -57,7 +59,17 @@ internal static List GetAttributes(ImmutableArray arg.Key == "ServiceType"); + if (!serviceTypeArgument.Value.IsNull) + return serviceTypeArgument.Value.Value as INamedTypeSymbol; + + // If ServiceType is not specified, infer from method's return type + return methodSymbol.ReturnType as INamedTypeSymbol; + } + + internal static ServiceLifetime? GetLifetimeFromAttribute(AttributeData attribute) { var lifetimeArgument = attribute.NamedArguments.FirstOrDefault(arg => arg.Key == "Lifetime"); if (!lifetimeArgument.Value.IsNull && IsLifetimeType(lifetimeArgument.Value.Type) && TryParseLifetime(lifetimeArgument.Value.Value, out var lifetime)) @@ -76,10 +88,10 @@ internal static List GetAttributes(ImmutableArray x.Type?.Name == typeof(TType).Name).Value; } + + internal static string GetReturnTypeFullName(IMethodSymbol methodSymbol) + { + return GetFullName(methodSymbol.ReturnType); + } } public record ServiceType(INamedTypeSymbol Type, string Name); \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Microsoft/README.md b/DependencyInjection.SourceGenerator.Microsoft/README.md deleted file mode 100644 index 50392b2..0000000 --- a/DependencyInjection.SourceGenerator.Microsoft/README.md +++ /dev/null @@ -1,202 +0,0 @@ -# DependencyInjection.SourceGenerator.Microsoft -Register services using attributes instead of registering in code. - -## Usage -Add the "Register" attribute to the class you want to register. The attribute takes a type and a lifetime. The type is the type you want to register and the lifetime is the lifetime of the service. The lifetime is optional and defaults to Transient. - -To use this library you need to install the source generator package and the contacts package. -The source generator package is a development dependency and will not be exposed as a dependency to consumers of your projects, while the contracts package contains the attributes and enums used to configure the generator. - - -```csharp - -namespace RootNamespace.Services; - -public interface IExampleService -{ - string GetExample(); -} - -public interface IAnotherService -{ - string GetAnother(); -} - -[Register(ServiceName = "ServiceName", Lifetime = Lifetime.Singleton)] -public class ExampleService : IExampleService -{ - public string GetExample() - { - return "Example"; - } -} - -[Decorate] -public class KeyedService : IExampleService -{ - public string GetExample() - { - return "Keyed"; - } -} - -[Decorator] -public class ServiceDecorator : IExampleService -{ - private readonly IExampleService _exampleService; - - public ServiceDecorator(IExampleService exampleService) - { - _exampleService = exampleService; - } - - public string GetExample() - { - return _exampleService.GetExample(); - } -} - -[Register] -public class MultipleInterfacesService : IExampleService, IAnotherService -{ - public string GetExample() - { - return "MultipleInterfaces"; - } - - public string GetAnother() - { - return "Another"; - } -} - -``` - - -Generates a class ServiceCollectionExtensions -Assuming the project is named MyProject, the generated method will be named AddMyProject. - -```csharp -// -#pragma warning disable -#nullable enable -namespace RootNamespace; -using global::Microsoft.Extensions.DependencyInjection; - -[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] -public static class ServiceCollectionExtensions -{ - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddMyProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.AddKeyedSingleton("ServiceName"); - services.Decorate(); - services.AddTransient(); - return services; - } -} -``` - -This can then be used like this: -```csharp -var services = new ServiceCollection(); -services.AddMyProject(); -``` - -for AspNetCore web APIs: -```csharp -public void ConfigureServices(IServiceCollection services) -{ - services.AddMyProject(); -} -``` - -and for minimal APIs: - -```csharp -var builder = WebApplication.CreateBuilder(args); -builder.Services.AddMyProject(); -``` - -You can also create a method that will be called by the AddMyProject method. This is useful if you want to register services from other libraries - -```csharp -using global::DependencyInjection.SourceGenerator.Contracts.Attributes; - -namespace DependencyInjection.SourceGenerator.Microsoft.Demo; - -public class Registrator -{ - [RegistrationExtension] - internal static void Register(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - // Register your additional services here - } -} -``` - -This will then produce the following code: - -```csharp -public static class ServiceCollectionExtensions -{ - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - global::DependencyInjection.SourceGenerator.Microsoft.Demo.Registrator.Register(services); - return services; - } -} -``` - -### Register all services in the project -You can also register all services in an project by adding the RegisterAll attribute to the assembly. This will register all implementations of the specified type. - -```csharp - -using DependencyInjection.SourceGenerator.Contracts.Attributes; - -[assembly: RegisterAll] - -namespace RootNamespace.Services; - -public interface IExampleService -{ - string GetExample(); -} - -public class ExampleService1 : IExampleService -{ - public string GetExample() - { - return "Example 1"; - } -} - -public class ExampleService2 : IExampleService -{ - public string GetExample() - { - return "Example 2"; - } -} - -``` - -this will generate the following code: - -```csharp -public static class ServiceCollectionExtensions -{ - public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) - { - services.AddTransient(); - services.AddTransient(); - return services; - } -} -``` - -## Lifetime -The lifetime is an enum with the following values: -- Transient -- Scoped -- Singleton \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Shared/ClassAttributeReceiver.cs b/DependencyInjection.SourceGenerator.Shared/ClassAttributeReceiver.cs deleted file mode 100644 index fe34b49..0000000 --- a/DependencyInjection.SourceGenerator.Shared/ClassAttributeReceiver.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis; -using System.Collections.Generic; -using System.Linq; -using DependencyInjection.SourceGenerator.Contracts.Attributes; -using System; - -namespace DependencyInjection.SourceGenerator.Shared; -public class ClassAttributeReceiver : ISyntaxContextReceiver -{ - private readonly string[] _classAttributes = [nameof(RegisterAttribute), nameof(DecorateAttribute)]; - private readonly string[] _methodAttributes = []; - private readonly string[] _assemblyAttributes = [nameof(RegisterAllAttribute)]; - - public List Types { get; } = []; - - public ClassAttributeReceiver(string[]? additionalAssemblyAttributes = null, string[]? additionalClassAttributes = null, string[]? additionalMethodAttributes = null) - { - if (additionalAssemblyAttributes is not null) - _assemblyAttributes = [.. _assemblyAttributes, .. additionalAssemblyAttributes]; - - if (additionalClassAttributes is not null) - _classAttributes = [.. _classAttributes, .. additionalClassAttributes]; - - if (additionalMethodAttributes is not null) - _methodAttributes = [.. _methodAttributes, .. additionalMethodAttributes]; - } - - - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) - { - if (context.Node is TypeDeclarationSyntax typeDeclarationSyntax) - EvaluateClass(context, typeDeclarationSyntax); - } - - private void EvaluateClass(GeneratorSyntaxContext context, TypeDeclarationSyntax typeDeclarationSyntax) - { - if (!HasAttribute(typeDeclarationSyntax)) - return; - - if (context.SemanticModel.GetDeclaredSymbol(typeDeclarationSyntax) is not INamedTypeSymbol typeSymbol) - { - return; - } - - Types.Add(typeSymbol); - } - - protected bool HasAttribute(TypeDeclarationSyntax classDeclarationSyntax) - { - var attributeLists = classDeclarationSyntax.AttributeLists; - if (HasAttribute(attributeLists, _classAttributes)) - return true; - - foreach (var member in classDeclarationSyntax.Members) - { - if (member is not MethodDeclarationSyntax methodDeclarationSyntax) - continue; - - if (HasAttribute(methodDeclarationSyntax.AttributeLists, _methodAttributes)) - return true; - } - - return false; - } - - private static bool HasAttribute(SyntaxList attributeLists, string[] attributes) - { - foreach (var attributeList in attributeLists) - { - foreach (var attribute in attributeList.Attributes) - { - var name = attribute.Name.ToString(); - if (attribute.Name is GenericNameSyntax genericNameSyntax) - name = genericNameSyntax.Identifier.ToString(); - - if (attributes.Contains(name) || attributes.Contains(name + "Attribute")) - return true; - } - } - return false; - } -} \ No newline at end of file diff --git a/DependencyInjection.SourceGenerator.Shared/DecorationMapper.cs b/DependencyInjection.SourceGenerator.Shared/DecorationMapper.cs deleted file mode 100644 index 904787c..0000000 --- a/DependencyInjection.SourceGenerator.Shared/DecorationMapper.cs +++ /dev/null @@ -1,25 +0,0 @@ -using DependencyInjection.SourceGenerator.Contracts.Attributes; -using Microsoft.CodeAnalysis; -using System.Linq; - -namespace DependencyInjection.SourceGenerator.Shared; -internal static class DecorationMapper -{ - internal static Decoration? CreateDecoration(INamedTypeSymbol type) - { - var attribute = TypeHelper.GetClassAttributes(type).FirstOrDefault(); - - if (attribute is null) - return null; - - var serviceType = TypeHelper.GetServiceType(type, attribute); - if (serviceType is null) - return null; - - return new Decoration - { - DecoratorTypeName = TypeHelper.GetFullName(type), - DecoratedTypeName = serviceType.Name - }; - } -} diff --git a/DependencyInjection.SourceGenerator.Shared/DependencyInjection.SourceGenerator.Shared.csproj b/DependencyInjection.SourceGenerator.Shared/DependencyInjection.SourceGenerator.Shared.csproj deleted file mode 100644 index 563526b..0000000 --- a/DependencyInjection.SourceGenerator.Shared/DependencyInjection.SourceGenerator.Shared.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - netstandard2.0 - 12 - enable - Nullable - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - <_Parameter1>DependencyInjection.SourceGenerator.LightInject - - - <_Parameter1>DependencyInjection.SourceGenerator.Microsoft - - - - - - - - diff --git a/DependencyInjection.SourceGenerator.Shared/Registration.cs b/DependencyInjection.SourceGenerator.Shared/Registration.cs deleted file mode 100644 index 2d383d2..0000000 --- a/DependencyInjection.SourceGenerator.Shared/Registration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using DependencyInjection.SourceGenerator.Contracts.Enums; -using System; - -namespace DependencyInjection.SourceGenerator.Shared; - -internal sealed class Registration -{ - public required string? ServiceType { get; init; } - public required string? ServiceName { get; init; } - public required Lifetime Lifetime { get; init; } - public required string ImplementationTypeName { get; init; } -} diff --git a/DependencyInjection.SourceGenerator.Shared/RegistrationCollector.cs b/DependencyInjection.SourceGenerator.Shared/RegistrationCollector.cs deleted file mode 100644 index 1e1547c..0000000 --- a/DependencyInjection.SourceGenerator.Shared/RegistrationCollector.cs +++ /dev/null @@ -1,140 +0,0 @@ -using DependencyInjection.SourceGenerator.Contracts.Attributes; -using DependencyInjection.SourceGenerator.Contracts.Enums; -using Microsoft.CodeAnalysis; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace DependencyInjection.SourceGenerator.Shared; -internal static class RegistrationCollector -{ - internal static List GetTypes(GeneratorExecutionContext context) - { - if (context.SyntaxContextReceiver is not ClassAttributeReceiver receiver) - { - return []; - } - - return receiver.Types; - } - - - internal static List GetRegisterAllTypes(GeneratorExecutionContext context) - { - var registerAllAttributes = TypeHelper.GetAttributes(context.Compilation.Assembly.GetAttributes()); - - var result = new List(); - foreach (var registerAllAttribute in registerAllAttributes) - { - var serviceType = TypeHelper.GetServiceTypeFromAttribute(registerAllAttribute); - if (serviceType is null) - continue; - - var lifetime = TypeHelper.GetLifetimeFromAttribute(registerAllAttribute) ?? Lifetime.Transient; - - var includeServiceName = TypeHelper.GetAttributeValue(registerAllAttribute, nameof(RegisterAllAttribute.IncludeServiceName)); - if (!bool.TryParse(includeServiceName?.ToString(), out var includeServiceNameValue)) - includeServiceNameValue = false; - - var registrations = GetImplementedTypes(serviceType, context, lifetime, includeServiceNameValue); - result.AddRange(registrations); - } - - return result; - } - - private static List GetImplementedTypes(INamedTypeSymbol? serviceType, GeneratorExecutionContext context, Lifetime lifetime, bool includeServiceNameValue) - { - var result = new List(); - if (serviceType is null) - return result; - - var compilation = context.Compilation; - foreach (var typeName in compilation.Assembly.TypeNames) - { - var types = compilation.GetSymbolsWithName(typeName, SymbolFilter.Type).OfType(); - foreach (var type in types) - { - if (type.IsAbstract) - continue; - - var registration = GetRegistration(type, serviceType, lifetime, includeServiceNameValue); - if (registration is not null) - result.Add(registration); - } - } - - return result; - } - - private static Registration? GetRegistration(INamedTypeSymbol type, INamedTypeSymbol serviceType, Lifetime lifetime, bool includeServiceNameValue) - { - if (serviceType.TypeKind == TypeKind.Interface && GetRegistrationByInterface(type, serviceType, lifetime, includeServiceNameValue) is { } registration) - return registration; - - if (serviceType.TypeKind == TypeKind.Class && GetBaseTypeImplementation(type, serviceType) is { } baseTypeImplementation) - { - return new Registration - { - ImplementationTypeName = TypeHelper.GetFullName(type), - Lifetime = lifetime, - ServiceName = includeServiceNameValue ? type.Name : null, - ServiceType = TypeHelper.GetFullName(baseTypeImplementation, type.ContainingNamespace) - }; - } - return null; - } - - private static Registration? GetRegistrationByInterface(INamedTypeSymbol type, INamedTypeSymbol serviceType, Lifetime lifetime, bool includeServiceNameValue) - { - var typeName = TypeHelper.GetFullName(type); - var serviceTypeName = TypeHelper.GetFullName(serviceType, type.ContainingNamespace); - foreach (var implInterface in type.AllInterfaces) - { - var implInterfaceName = TypeHelper.GetFullName(implInterface, type.ContainingNamespace); - if (serviceType.IsUnboundGenericType) - { - if (implInterface.IsGenericType && TypeHelper.GetFullName(implInterface.ConstructUnboundGenericType(), type.ContainingNamespace) == serviceTypeName) - { - return new Registration - { - ImplementationTypeName = typeName, - Lifetime = lifetime, - ServiceName = includeServiceNameValue ? type.Name : null, - ServiceType = implInterfaceName - }; - } - } - else if (implInterfaceName == serviceTypeName) - { - return new Registration - { - ImplementationTypeName = typeName, - Lifetime = lifetime, - ServiceName = includeServiceNameValue ? type.Name : null, - ServiceType = serviceTypeName - }; - } - } - return null; - } - - private static INamedTypeSymbol? GetBaseTypeImplementation(INamedTypeSymbol type, INamedTypeSymbol typeToCheck) - { - if (type.BaseType is not { } baseType) - return null; ; - - if (typeToCheck.IsUnboundGenericType) - { - if (baseType.IsGenericType && TypeHelper.GetFullName(baseType.ConstructUnboundGenericType(), type.ContainingNamespace) == TypeHelper.GetFullName(typeToCheck, type.ContainingNamespace)) - return baseType; - - return GetBaseTypeImplementation(baseType, typeToCheck); - } - - if (TypeHelper.GetFullName(baseType, type.ContainingNamespace) == TypeHelper.GetFullName(typeToCheck, type.ContainingNamespace)) - return baseType; - - return GetBaseTypeImplementation(baseType, typeToCheck); - } -} diff --git a/DependencyInjection.SourceGenerator.Shared/RegistrationMapper.cs b/DependencyInjection.SourceGenerator.Shared/RegistrationMapper.cs deleted file mode 100644 index f1c4ef8..0000000 --- a/DependencyInjection.SourceGenerator.Shared/RegistrationMapper.cs +++ /dev/null @@ -1,53 +0,0 @@ -using DependencyInjection.SourceGenerator.Contracts.Attributes; -using DependencyInjection.SourceGenerator.Contracts.Enums; -using Microsoft.CodeAnalysis; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace DependencyInjection.SourceGenerator.Shared; -internal static class RegistrationMapper -{ - internal static List CreateRegistration(INamedTypeSymbol type) - { - var attributes = TypeHelper.GetClassAttributes(type); - - if (!attributes.Any()) - return []; - - var result = new List(); - foreach (var attribute in attributes) - { - - var serviceType = TypeHelper.GetServiceType(type, attribute); - if (serviceType is null) - continue; - - var lifetime = TypeHelper.GetLifetimeFromAttribute(attribute) ?? Lifetime.Transient; - - var serviceNameArgument = TypeHelper.GetAttributeValue(attribute, nameof(RegisterAttribute.ServiceName)); - - // Get the value of the property - var serviceName = serviceNameArgument?.ToString(); - - var implementationTypeName = TypeHelper.GetFullName(type); - - if (TypeHelper.IsSameType(type, serviceType.Type)) - { - implementationTypeName = serviceType.Name; - serviceType = null; - } - - var registration = new Registration - { - ImplementationTypeName = implementationTypeName, - Lifetime = lifetime, - ServiceName = serviceName, - ServiceType = serviceType?.Name - }; - result.Add(registration); - } - - return result; - } -} diff --git a/DependencyInjection.SourceGenerator.sln b/DependencyInjection.SourceGenerator.sln index f51c90d..46d6106 100644 --- a/DependencyInjection.SourceGenerator.sln +++ b/DependencyInjection.SourceGenerator.sln @@ -3,31 +3,20 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.33103.201 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyInjection.SourceGenerator.LightInject", "DependencyInjection.SourceGenerator.LightInject\DependencyInjection.SourceGenerator.LightInject.csproj", "{16627481-D144-4178-9E5D-177E4D0DFE81}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyInjection.SourceGenerator.Contracts", "DependencyInjection.SourceGenerator.Contracts\DependencyInjection.SourceGenerator.Contracts.csproj", "{E62B793F-7BD6-410B-BEBF-0E9EDC8BCC28}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Files", "Files", "{5387CF4F-733F-4A71-A82B-D4733964A06A}" ProjectSection(SolutionItems) = preProject - common.targets = common.targets .github\workflows\dotnet.yml = .github\workflows\dotnet.yml LICENSE = LICENSE README.md = README.md EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyInjection.SourceGenerator.LightInject.Tests", "DependencyInjection.SourceGenerator.LightInject.Tests\DependencyInjection.SourceGenerator.LightInject.Tests.csproj", "{2C2D60A1-7E80-493D-8123-187769EEE855}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyInjection.SourceGenerator.Shared", "DependencyInjection.SourceGenerator.Shared\DependencyInjection.SourceGenerator.Shared.csproj", "{671E49C0-7C64-4FC0-BDB9-0F486CDDD0B7}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyInjection.SourceGenerator.Microsoft", "DependencyInjection.SourceGenerator.Microsoft\DependencyInjection.SourceGenerator.Microsoft.csproj", "{90EAE615-66EA-4AC4-B25C-6970F017700A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DependencyInjection.SourceGenerator.Microsoft.Tests", "DependencyInjection.SourceGenerator.Microsoft.Tests\DependencyInjection.SourceGenerator.Microsoft.Tests.csproj", "{E42AD1F2-3243-4E90-9C01-07E38577925E}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{307324C8-0321-4E96-8C4B-5566A672364A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyInjection.SourceGenerator.LightInject.Contracts", "DependencyInjection.SourceGenerator.LightInject.Contracts\DependencyInjection.SourceGenerator.LightInject.Contracts.csproj", "{87DFF61F-C561-4587-89B8-D360B44A257F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyInjection.SourceGenerator.Microsoft.Contracts", "DependencyInjection.SourceGenerator.Microsoft.Contracts\DependencyInjection.SourceGenerator.Microsoft.Contracts.csproj", "{15C145AF-074C-4607-B09D-1B1FB3AEE9D2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyInjection.SourceGenerator.Microsoft.Demo", "DependencyInjection.SourceGenerator.Microsoft.Demo\DependencyInjection.SourceGenerator.Microsoft.Demo.csproj", "{5F349F60-524E-49C2-BF0A-D50C2491AB49}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -35,22 +24,6 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {16627481-D144-4178-9E5D-177E4D0DFE81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {16627481-D144-4178-9E5D-177E4D0DFE81}.Debug|Any CPU.Build.0 = Debug|Any CPU - {16627481-D144-4178-9E5D-177E4D0DFE81}.Release|Any CPU.ActiveCfg = Release|Any CPU - {16627481-D144-4178-9E5D-177E4D0DFE81}.Release|Any CPU.Build.0 = Release|Any CPU - {E62B793F-7BD6-410B-BEBF-0E9EDC8BCC28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E62B793F-7BD6-410B-BEBF-0E9EDC8BCC28}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E62B793F-7BD6-410B-BEBF-0E9EDC8BCC28}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E62B793F-7BD6-410B-BEBF-0E9EDC8BCC28}.Release|Any CPU.Build.0 = Release|Any CPU - {2C2D60A1-7E80-493D-8123-187769EEE855}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2C2D60A1-7E80-493D-8123-187769EEE855}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2C2D60A1-7E80-493D-8123-187769EEE855}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2C2D60A1-7E80-493D-8123-187769EEE855}.Release|Any CPU.Build.0 = Release|Any CPU - {671E49C0-7C64-4FC0-BDB9-0F486CDDD0B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {671E49C0-7C64-4FC0-BDB9-0F486CDDD0B7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {671E49C0-7C64-4FC0-BDB9-0F486CDDD0B7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {671E49C0-7C64-4FC0-BDB9-0F486CDDD0B7}.Release|Any CPU.Build.0 = Release|Any CPU {90EAE615-66EA-4AC4-B25C-6970F017700A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {90EAE615-66EA-4AC4-B25C-6970F017700A}.Debug|Any CPU.Build.0 = Debug|Any CPU {90EAE615-66EA-4AC4-B25C-6970F017700A}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -59,21 +32,17 @@ Global {E42AD1F2-3243-4E90-9C01-07E38577925E}.Debug|Any CPU.Build.0 = Debug|Any CPU {E42AD1F2-3243-4E90-9C01-07E38577925E}.Release|Any CPU.ActiveCfg = Release|Any CPU {E42AD1F2-3243-4E90-9C01-07E38577925E}.Release|Any CPU.Build.0 = Release|Any CPU - {87DFF61F-C561-4587-89B8-D360B44A257F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {87DFF61F-C561-4587-89B8-D360B44A257F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {87DFF61F-C561-4587-89B8-D360B44A257F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {87DFF61F-C561-4587-89B8-D360B44A257F}.Release|Any CPU.Build.0 = Release|Any CPU - {15C145AF-074C-4607-B09D-1B1FB3AEE9D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {15C145AF-074C-4607-B09D-1B1FB3AEE9D2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {15C145AF-074C-4607-B09D-1B1FB3AEE9D2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {15C145AF-074C-4607-B09D-1B1FB3AEE9D2}.Release|Any CPU.Build.0 = Release|Any CPU + {5F349F60-524E-49C2-BF0A-D50C2491AB49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F349F60-524E-49C2-BF0A-D50C2491AB49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F349F60-524E-49C2-BF0A-D50C2491AB49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F349F60-524E-49C2-BF0A-D50C2491AB49}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {2C2D60A1-7E80-493D-8123-187769EEE855} = {307324C8-0321-4E96-8C4B-5566A672364A} {E42AD1F2-3243-4E90-9C01-07E38577925E} = {307324C8-0321-4E96-8C4B-5566A672364A} + {5F349F60-524E-49C2-BF0A-D50C2491AB49} = {307324C8-0321-4E96-8C4B-5566A672364A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6DC31EA4-A125-4CEA-9C7F-C266B28473ED} diff --git a/MigrationGuide.md b/MigrationGuide.md new file mode 100644 index 0000000..23adc29 --- /dev/null +++ b/MigrationGuide.md @@ -0,0 +1,51 @@ +# Migration Guide + +## Overview +The `DependencyInjection.SourceGenerator.Microsoft.Contracts` package has been deprecated. The attributes and enums previously provided by this package are now generated as internal attributes in each project. This change eliminates the possibility of version conflicts. + +## Changes + +### Namespace Changes +- Old Namespace: `DependencyInjection.SourceGenerator.Microsoft.Contracts.Attributes` +- New Namespace: `Microsoft.Extensions.DependencyInjection` + +### Enum Changes +- Old Enum: `DependencyInjection.SourceGenerator.Microsoft.Contracts.Enums.Lifetime` +- New Enum: `Microsoft.Extensions.DependencyInjection.ServiceLifetime` + +## Migration Steps + +1. **Remove the old package:** + ```sh + dotnet remove package DependencyInjection.SourceGenerator.Microsoft.Contracts + ``` + +2. **Update your code to use the new namespaces and enums:** + + - Replace: + ```csharp + using DependencyInjection.SourceGenerator.Microsoft.Contracts.Attributes; + using DependencyInjection.SourceGenerator.Microsoft.Contracts.Enums; + ``` + + - With: + ```csharp + using Microsoft.Extensions.DependencyInjection; + ``` + +3. **Update attribute usage:** + - Old: + ```csharp + [Register(Lifetime = Lifetime.Singleton)] + ``` + - New: + ```csharp + [Register(Lifetime = ServiceLifetime.Singleton)] + ``` + +4. **Rebuild your project:** + ```sh + dotnet build + ``` + +Following these steps will help you migrate to the new version of the library without any issues. diff --git a/README.md b/README.md index e021c55..bfb6609 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,362 @@ -# DependencyInjection.SourceGenerator +# DependencyInjection.SourceGenerator.Microsoft Register services using attributes instead of registering in code. +This library simplifies dependency injection in .NET applications by moving service registration from startup code to the service classes themselves using attributes. This approach provides compile-time validation, reduces boilerplate code, and keeps service registration logic next to the implementation where it belongs. With features like automatic registration of interface implementations, decorator pattern support, and factory registration, it helps maintain clean and maintainable dependency injection configuration, especially in larger applications. + ## Usage Add the "Register" attribute to the class you want to register. The attribute takes a type and a lifetime. The type is the type you want to register and the lifetime is the lifetime of the service. The lifetime is optional and defaults to Transient. This library supports the following dependency injection frameworks, follow the links for more information on how to use them: -- [Microsoft.Extensions.DependencyInjection](DependencyInjection.SourceGenerator.Microsoft/README.md) -- [LightInject](DependencyInjection.SourceGenerator.LightInject/README.md) +- [Microsoft.Extensions.DependencyInjection](#microsoftextensionsdependencyinjection) -To use this library you need to install the source generator package and the contacts package. +To use this library you need to install the source generator package and the contracts package. The source generator package is a development dependency and will not be exposed as a dependency to consumers of your projects, while the contracts package contains the attributes and enums used to configure the generator. +**Note:** The `DependencyInjection.SourceGenerator.Microsoft.Contracts` package has been deprecated. Please refer to the [Migration Guide](MigrationGuide.md) for more information. + ### Microsoft.Extensions.DependencyInjection * #### DependencyInjection.SourceGenerator.Microsoft [![NuGet](https://img.shields.io/nuget/vpre/DependencyInjection.SourceGenerator.Microsoft.svg)](https://www.nuget.org/packages/DependencyInjection.SourceGenerator.Microsoft) * #### DependencyInjection.SourceGenerator.Microsoft.Contracts [![NuGet](https://img.shields.io/nuget/vpre/DependencyInjection.SourceGenerator.Microsoft.Contracts.svg)](https://www.nuget.org/packages/DependencyInjection.SourceGenerator.Microsoft.Contracts) -### LightInject -* #### DependencyInjection.SourceGenerator.LightInject [![NuGet](https://img.shields.io/nuget/vpre/DependencyInjection.SourceGenerator.LightInject.svg)](https://www.nuget.org/packages/DependencyInjection.SourceGenerator.LightInject) -* #### DependencyInjection.SourceGenerator.LightInject.Contracts [![NuGet](https://img.shields.io/nuget/vpre/DependencyInjection.SourceGenerator.LightInject.Contracts.svg)](https://www.nuget.org/packages/DependencyInjection.SourceGenerator.LightInject.Contracts) +```csharp +namespace RootNamespace.Services; + +public interface IExampleService +{ + string GetExample(); +} + +public interface IAnotherService +{ + string GetAnother(); +} + +[Register(ServiceName = "ServiceName", Lifetime = Lifetime.Singleton)] +public class ExampleService : IExampleService +{ + public string GetExample() + { + return "Example"; + } +} + +[Decorate] +public class KeyedService : IExampleService +{ + public string GetExample() + { + return "Keyed"; + } +} + +[Decorator] +public class ServiceDecorator : IExampleService +{ + private readonly IExampleService _exampleService; + + public ServiceDecorator(IExampleService exampleService) + { + _exampleService = exampleService; + } + + public string GetExample() + { + return _exampleService.GetExample(); + } +} + +[Register] +public class MultipleInterfacesService : IExampleService, IAnotherService +{ + public string GetExample() + { + return "MultipleInterfaces"; + } + + public string GetAnother() + { + return "Another"; + } +} +``` + +Generates a class ServiceCollectionExtensions +Assuming the project is named MyProject, the generated method will be named AddMyProject. + +```csharp +// +#pragma warning disable +#nullable enable +namespace RootNamespace; +using global::Microsoft.Extensions.DependencyInjection; + +[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +public static class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddMyProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddKeyedSingleton("ServiceName"); + services.Decorate(); + services.AddTransient(); + return services; + } +} +``` -Both contracts packages references the shared contracts package, which contains the attributes and enums used to configure the generator. -* #### DependencyInjection.SourceGenerator.Contracts [![NuGet](https://img.shields.io/nuget/vpre/DependencyInjection.SourceGenerator.Contracts.svg)](https://www.nuget.org/packages/DependencyInjection.SourceGenerator.Contracts) +You can then use the generated extension method as follows: +```csharp +var services = new ServiceCollection(); +services.AddMyProject(); +``` + +for AspNetCore web APIs: +```csharp +public void ConfigureServices(IServiceCollection services) +{ + services.AddMyProject(); +} +``` + +and for minimal APIs: + +```csharp +var builder = WebApplication.CreateBuilder(args); +builder.Services.AddMyProject(); +``` + +### Register all services in the project +You can also register all services in a project by adding the RegisterAll attribute to the assembly. This will register all implementations of the specified interface or base type. + +```csharp +using DependencyInjection.SourceGenerator.Contracts.Attributes; + +[assembly: RegisterAll] + +namespace RootNamespace.Services; + +public interface IExampleService +{ + string GetExample(); +} + +public class ExampleService1 : IExampleService +{ + public string GetExample() + { + return "Example 1"; + } +} + +public class ExampleService2 : IExampleService +{ + public string GetExample() + { + return "Example 2"; + } +} +``` + +this will generate the following code: + +```csharp +public static class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + return services; + } +} +``` ## Lifetime -The lifetime is an enum with the following values: +The `Lifetime` is an enum with the following values: - Transient - Scoped - Singleton + +## IncludeFactory +The `IncludeFactory` property allows you to register a service as a `Func`, enabling you to inject a factory method into your constructors. This is useful when you need to create multiple instances of a service on demand. + +### Example +```csharp +namespace RootNamespace.Services; + +public interface IExampleService +{ + string GetExample(); +} + +[Register(IncludeFactory = true)] +public class ExampleService : IExampleService +{ + public string GetExample() + { + return "Example"; + } +} +``` + +This will generate the following code: +```csharp +public static class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddMyProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + services.AddTransient>(provider => () => provider.GetRequiredService()); + return services; + } +} +``` + +You can then inject `Func` into your constructors: +```csharp +public class Consumer +{ + private readonly Func _exampleServiceFactory; + + public Consumer(Func exampleServiceFactory) + { + _exampleServiceFactory = exampleServiceFactory; + } + + public void UseService() + { + var serviceInstance = _exampleServiceFactory(); + // Use the service instance + } +} +``` + +### RegisterAll with IncludeFactory +You can also use `IncludeFactory` with the `RegisterAll` attribute to register all implementations of a specified type along with their factory methods. + +```csharp +using DependencyInjection.SourceGenerator.Contracts.Attributes; + +[assembly: RegisterAll(IncludeFactory = true)] + +namespace RootNamespace.Services; + +public interface IExampleService +{ + string GetExample(); +} + +public class ExampleService1 : IExampleService +{ + public string GetExample() + { + return "Example 1"; + } +} + +public class ExampleService2 : IExampleService +{ + public string GetExample() + { + return "Example 2"; + } +} +``` + +This will generate the following code: +```csharp +public static class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddMyProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(); + services.AddTransient(); + services.AddTransient>(provider => () => provider.GetRequiredService()); + return services; + } +} +``` + +You can then inject `Func` into your constructors as shown in the previous example. + +## Method Registrations +You can also register services using static methods. The method must be static, public or internal, and must have a single parameter of type `IServiceProvider` or `IServiceCollection`. + +### Example with IServiceProvider +```csharp +namespace RootNamespace.Services; + +public interface IExampleService +{ + string GetExample(); +} + +public class ExampleService : IExampleService +{ + public string GetExample() + { + return "Example"; + } +} + +public static class ServiceRegistrations +{ + [Register] + public static IExampleService RegisterExampleService(IServiceProvider services) + { + return new ExampleService(); + } +} +``` + +This will generate the following code: +```csharp +public static class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddMyProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + services.AddTransient(global::RootNamespace.Services.ServiceRegistrations.RegisterExampleService); + return services; + } +} +``` + +### Example with IServiceCollection +```csharp +namespace RootNamespace.Services; + +public interface IExampleService +{ + string GetExample(); +} + +public class ExampleService : IExampleService +{ + public string GetExample() + { + return "Example"; + } +} + +public static class ServiceRegistrations +{ + [Register] + public static void RegisterExampleService(IServiceCollection services) + { + services.AddTransient(); + } +} +``` + +This will generate the following code: +```csharp +public static class ServiceCollectionExtensions +{ + public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddMyProject(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) + { + global::RootNamespace.Services.ServiceRegistrations.RegisterExampleService(services); + return services; + } +} +``` + +You can then use the generated extension method as shown in the previous examples. diff --git a/common.targets b/common.targets index e09b351..48c0154 100644 --- a/common.targets +++ b/common.targets @@ -8,7 +8,7 @@ 12 true LICENSE - 2.0.0 + 3.0.0-alpha1