├── ServiceScan.SourceGenerator ├── Model │ ├── MethodWithAttributesModel.cs │ ├── MethodImplementationModel.cs │ ├── DiagnosticModel.cs │ ├── ServiceRegistrationModel.cs │ ├── MethodModel.cs │ └── AttributeModel.cs ├── SymbolExtensions.cs ├── CombinedProviderComparer.cs ├── Extensions │ ├── StringExtensions.cs │ └── TypeSymbolExtensions.cs ├── ServiceScan.SourceGenerator.csproj ├── EquatableArray.cs ├── DiagnosticDescriptors.cs ├── DependencyInjectionGenerator.ParseMethodModel.cs ├── GenerateAttributeInfo.cs ├── DependencyInjectionGenerator.FindServicesToRegister.cs ├── DependencyInjectionGenerator.cs └── DependencyInjectionGenerator.FilterTypes.cs ├── ServiceScan.SourceGenerator.Playground ├── Services.cs ├── ServiceCollectionExtensions.cs └── ServiceScan.SourceGenerator.Playground.csproj ├── ServiceScan.SourceGenerator.Tests ├── TestServices.cs ├── ServiceScan.SourceGenerator.Tests.csproj ├── Sources.cs ├── GeneratedMethodTests.cs ├── DiagnosticTests.cs ├── CustomHandlerTests.cs └── AddServicesTests.cs ├── version.json ├── Directory.Build.props ├── .github └── workflows │ └── dotnet.yml ├── LICENSE.txt ├── .gitattributes ├── ServiceScan.SourceGenerator.sln ├── .gitignore ├── README.md └── .editorconfig /ServiceScan.SourceGenerator/Model/MethodWithAttributesModel.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceScan.SourceGenerator.Model; 2 | 3 | record MethodWithAttributesModel(MethodModel Method, EquatableArray Attributes); 4 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator.Playground/Services.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceScan.SourceGenerator.Playground; 2 | 3 | public interface IService { } 4 | public class MyService1 : IService { } 5 | public class MyService2 : IService { } 6 | public class MyServ111111111ice3 : IService { } -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/Model/MethodImplementationModel.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceScan.SourceGenerator.Model; 2 | 3 | record MethodImplementationModel( 4 | MethodModel Method, 5 | EquatableArray Registrations, 6 | EquatableArray CustomHandlers); 7 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator.Tests/TestServices.cs: -------------------------------------------------------------------------------- 1 | namespace External; 2 | 3 | public interface IExternalService; 4 | public class ExternalService1 : IExternalService { } 5 | public class ExternalService2 : IExternalService { } 6 | 7 | // Shouldn't be added as type is not accessible from other assembly 8 | internal class InternalExternalService2 : IExternalService { } -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "2.4", 4 | "publicReleaseRefSpec": [ 5 | "^refs/heads/main", 6 | "^refs/heads/v\\d+(?:\\.\\d+)?$" 7 | ], 8 | "cloudBuild": { 9 | "buildNumber": { 10 | "enabled": true 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | all 6 | 3.7.112 7 | 8 | 9 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/Model/DiagnosticModel.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace ServiceScan.SourceGenerator.Model; 4 | 5 | record DiagnosticModel(Diagnostic? Diagnostic, T? Model) 6 | { 7 | public static implicit operator DiagnosticModel(T model) => new(null, model); 8 | public static implicit operator DiagnosticModel(Diagnostic diagnostic) => new(diagnostic, default); 9 | } 10 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator.Playground/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace ServiceScan.SourceGenerator.Playground; 4 | 5 | public static partial class ServiceCollectionExtensions 6 | { 7 | [GenerateServiceRegistrations(AssignableTo = typeof(IService), TypeNameFilter = "*Ser*")] 8 | public static partial IServiceCollection AddServices(this IServiceCollection services); 9 | } 10 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/SymbolExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace ServiceScan.SourceGenerator; 4 | 5 | public static class SymbolExtensions 6 | { 7 | public static string ToFullMetadataName(this ISymbol symbol) 8 | { 9 | return symbol.ContainingNamespace.IsGlobalNamespace 10 | ? symbol.MetadataName 11 | : symbol.ContainingNamespace.ToDisplayString() + "." + symbol.MetadataName; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/Model/ServiceRegistrationModel.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceScan.SourceGenerator.Model; 2 | 3 | record ServiceRegistrationModel( 4 | string Lifetime, 5 | string ServiceTypeName, 6 | string ImplementationTypeName, 7 | bool ResolveImplementation, 8 | bool IsOpenGeneric, 9 | string? KeySelector, 10 | KeySelectorType? KeySelectorType); 11 | 12 | record CustomHandlerModel( 13 | CustomHandlerType CustomHandlerType, 14 | string HandlerMethodName, 15 | string TypeName, 16 | EquatableArray TypeArguments); 17 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator.Playground/ServiceScan.SourceGenerator.Playground.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/CombinedProviderComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using ServiceScan.SourceGenerator.Model; 3 | using Microsoft.CodeAnalysis; 4 | 5 | namespace ServiceScan.SourceGenerator; 6 | 7 | using CombinedModel = (DiagnosticModel Model, Compilation Compilation); 8 | 9 | // We only compare Model here and ignore Compilation, as I don't want to run it on every input. 10 | internal class CombinedProviderComparer : IEqualityComparer 11 | { 12 | public static readonly CombinedProviderComparer Instance = new(); 13 | 14 | public bool Equals(CombinedModel x, CombinedModel y) 15 | { 16 | return x.Model.Equals(y.Model); 17 | } 18 | 19 | public int GetHashCode(CombinedModel obj) 20 | { 21 | return obj.Model.GetHashCode(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: .NET 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 # avoid shallow clone so nbgv can do its work. 21 | - name: Setup .NET 22 | uses: actions/setup-dotnet@v4 23 | with: 24 | dotnet-version: 8.0.x 25 | - name: Restore dependencies 26 | run: dotnet restore 27 | - name: Build 28 | run: dotnet build --no-restore -c Release 29 | - name: Test 30 | run: dotnet test --no-build -c Release --verbosity normal 31 | - name: Upload a Build Artifact 32 | uses: actions/upload-artifact@v4.3.3 33 | with: 34 | path: 'ServiceScan.SourceGenerator/bin/Release/*.nupkg' 35 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator.Tests/ServiceScan.SourceGenerator.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ServiceScan.SourceGenerator.Extensions; 4 | 5 | internal static class StringExtensions 6 | { 7 | public static string ReplaceLineEndings(this string input) 8 | { 9 | #if NET6_0_OR_GREATER 10 | return input.ReplaceLineEndings(); 11 | #else 12 | #pragma warning disable RS1035 // Do not use APIs banned for analyzers 13 | return ReplaceLineEndings(input, Environment.NewLine); 14 | #pragma warning restore RS1035 // Do not use APIs banned for analyzers 15 | #endif 16 | } 17 | 18 | public static string ReplaceLineEndings(this string input, string replacementText) 19 | { 20 | #if NET6_0_OR_GREATER 21 | return input.ReplaceLineEndings(replacementText); 22 | #else 23 | // First normalize to LF 24 | var lineFeedInput = input 25 | .Replace("\r\n", "\n") 26 | .Replace("\r", "\n"); 27 | 28 | // Then normalize to the replacement text 29 | return lineFeedInput.Replace("\n", replacementText); 30 | #endif 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator.Tests/Sources.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceScan.SourceGenerator.Tests; 2 | 3 | public static class Sources 4 | { 5 | public static string MethodWithAttribute(string attribute) 6 | { 7 | attribute = attribute.Replace("\n", "\n "); 8 | 9 | return $$""" 10 | using ServiceScan.SourceGenerator; 11 | using Microsoft.Extensions.DependencyInjection; 12 | 13 | namespace GeneratorTests; 14 | 15 | public static partial class ServicesExtensions 16 | { 17 | {{attribute}} 18 | public static partial IServiceCollection AddServices(this IServiceCollection services); 19 | } 20 | """; 21 | } 22 | 23 | public static string GetMethodImplementation(string services) 24 | { 25 | services = services.Replace("\n", "\n "); 26 | 27 | return $$""" 28 | using Microsoft.Extensions.DependencyInjection; 29 | 30 | namespace GeneratorTests; 31 | 32 | public static partial class ServicesExtensions 33 | { 34 | public static partial IServiceCollection AddServices(this IServiceCollection services) 35 | { 36 | {{services}} 37 | } 38 | } 39 | """; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/Extensions/TypeSymbolExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.CodeAnalysis; 3 | 4 | namespace ServiceScan.SourceGenerator.Extensions; 5 | 6 | internal static class TypeSymbolExtensions 7 | { 8 | /// 9 | /// Retrieves a method symbol from the specified type by name, considering accessibility, static context, and 10 | /// inheritance. 11 | /// 12 | /// This method searches the specified type and its base types for a method with the given name 13 | /// that matches the specified accessibility and static context. If no matching method is found, the method returns 14 | /// . 15 | public static IMethodSymbol? GetMethod(this ITypeSymbol type, string methodName, SemanticModel semanticModel, int position, bool? isStatic = null) 16 | { 17 | var currentType = type; 18 | 19 | while (currentType != null) 20 | { 21 | var method = currentType.GetMembers() 22 | .OfType() 23 | .Where(m => m.Name == methodName 24 | && (isStatic == null || m.IsStatic == isStatic) 25 | && semanticModel.IsAccessible(position, m)) 26 | .FirstOrDefault(); 27 | 28 | if (method != null) 29 | return method; 30 | 31 | currentType = currentType.BaseType; 32 | } 33 | 34 | return null; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/Model/MethodModel.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp.Syntax; 4 | 5 | namespace ServiceScan.SourceGenerator.Model; 6 | 7 | record ParameterModel(string Type, string Name); 8 | 9 | record MethodModel( 10 | string? Namespace, 11 | string TypeName, 12 | string TypeMetadataName, 13 | string TypeModifiers, 14 | string MethodName, 15 | string MethodModifiers, 16 | EquatableArray Parameters, 17 | bool IsExtensionMethod, 18 | bool ReturnsVoid, 19 | string ReturnType) 20 | { 21 | public string ParameterName => Parameters.First().Name; 22 | 23 | public static MethodModel Create(IMethodSymbol method, SyntaxNode syntax) 24 | { 25 | EquatableArray parameters = [.. method.Parameters 26 | .Select(p => new ParameterModel(p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), p.Name))]; 27 | 28 | var typeSyntax = syntax.FirstAncestorOrSelf(); 29 | 30 | return new MethodModel( 31 | Namespace: method.ContainingNamespace.IsGlobalNamespace ? null : method.ContainingNamespace.ToDisplayString(), 32 | TypeName: method.ContainingType.Name, 33 | TypeMetadataName: method.ContainingType.ToFullMetadataName(), 34 | TypeModifiers: GetModifiers(typeSyntax), 35 | MethodName: method.Name, 36 | MethodModifiers: GetModifiers(syntax), 37 | Parameters: parameters, 38 | IsExtensionMethod: method.IsExtensionMethod, 39 | ReturnsVoid: method.ReturnsVoid, 40 | ReturnType: method.ReturnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); 41 | } 42 | 43 | private static string GetModifiers(SyntaxNode? syntax) 44 | { 45 | return (syntax as MemberDeclarationSyntax)?.Modifiers.ToString() ?? ""; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/ServiceScan.SourceGenerator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 12.0 6 | true 7 | annotations 8 | true 9 | True 10 | MIT 11 | True 12 | True 13 | README.md 14 | https://github.com/Dreamescaper/ServiceScan.SourceGenerator 15 | false 16 | https://github.com/Dreamescaper/ServiceScan.SourceGenerator 17 | Oleksandr Liakhevych 18 | DependencyInjection;SourceGenerator 19 | Types scanning source generator for Microsoft.Extensions.DependencyInjection 20 | https://github.com/Dreamescaper/ServiceScan.SourceGenerator/releases 21 | 22 | 23 | 24 | 25 | True 26 | \ 27 | 28 | 29 | 30 | 31 | 32 | 33 | all 34 | runtime; build; native; contentfiles; analyzers; buildtransitive 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.34825.169 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B4814492-C04A-4DDF-8C13-0F2A009ADBBF}" 7 | ProjectSection(SolutionItems) = preProject 8 | .editorconfig = .editorconfig 9 | Directory.Build.props = Directory.Build.props 10 | .github\workflows\dotnet.yml = .github\workflows\dotnet.yml 11 | README.md = README.md 12 | version.json = version.json 13 | EndProjectSection 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceScan.SourceGenerator", "ServiceScan.SourceGenerator\ServiceScan.SourceGenerator.csproj", "{91F5378F-D636-463A-8F65-43A039BB95DC}" 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceScan.SourceGenerator.Playground", "ServiceScan.SourceGenerator.Playground\ServiceScan.SourceGenerator.Playground.csproj", "{957A7F4D-8CA7-4C40-90BE-CFF590342436}" 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceScan.SourceGenerator.Tests", "ServiceScan.SourceGenerator.Tests\ServiceScan.SourceGenerator.Tests.csproj", "{C837E2E7-243C-48AE-B439-92CA710477BF}" 20 | EndProject 21 | Global 22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 23 | Debug|Any CPU = Debug|Any CPU 24 | Release|Any CPU = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {91F5378F-D636-463A-8F65-43A039BB95DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {91F5378F-D636-463A-8F65-43A039BB95DC}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {91F5378F-D636-463A-8F65-43A039BB95DC}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {91F5378F-D636-463A-8F65-43A039BB95DC}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {957A7F4D-8CA7-4C40-90BE-CFF590342436}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {957A7F4D-8CA7-4C40-90BE-CFF590342436}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {957A7F4D-8CA7-4C40-90BE-CFF590342436}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {957A7F4D-8CA7-4C40-90BE-CFF590342436}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {C837E2E7-243C-48AE-B439-92CA710477BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {C837E2E7-243C-48AE-B439-92CA710477BF}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {C837E2E7-243C-48AE-B439-92CA710477BF}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {C837E2E7-243C-48AE-B439-92CA710477BF}.Release|Any CPU.Build.0 = Release|Any CPU 39 | EndGlobalSection 40 | GlobalSection(SolutionProperties) = preSolution 41 | HideSolutionNode = FALSE 42 | EndGlobalSection 43 | GlobalSection(ExtensibilityGlobals) = postSolution 44 | SolutionGuid = {97D548F3-75D2-436F-9FC4-0770B4080DEA} 45 | EndGlobalSection 46 | EndGlobal 47 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/EquatableArray.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Collections.Immutable; 5 | using System.Runtime.CompilerServices; 6 | 7 | namespace ServiceScan.SourceGenerator; 8 | 9 | /// 10 | /// Creates a new instance. 11 | /// 12 | /// The input to wrap. 13 | [CollectionBuilder(typeof(EquatableArrayBuilder), nameof(EquatableArrayBuilder.Create))] 14 | internal readonly struct EquatableArray(T[] array) : IEquatable>, IEnumerable where T : IEquatable 15 | { 16 | public static readonly EquatableArray Empty = new([]); 17 | 18 | /// 19 | public bool Equals(EquatableArray array) 20 | { 21 | return AsSpan().SequenceEqual(array.AsSpan()); 22 | } 23 | 24 | /// 25 | public override bool Equals(object? obj) 26 | { 27 | return obj is EquatableArray array && Equals(this, array); 28 | } 29 | 30 | /// 31 | public override int GetHashCode() 32 | { 33 | int hashCode = 0; 34 | 35 | for (int i = 0; i < array.Length; i++) 36 | hashCode ^= array[i].GetHashCode(); 37 | 38 | return hashCode; 39 | } 40 | 41 | /// 42 | /// Returns a wrapping the current items. 43 | /// 44 | /// A wrapping the current items. 45 | public ReadOnlySpan AsSpan() 46 | { 47 | return array.AsSpan(); 48 | } 49 | 50 | /// 51 | IEnumerator IEnumerable.GetEnumerator() 52 | { 53 | return ((IEnumerable)(array ?? [])).GetEnumerator(); 54 | } 55 | 56 | /// 57 | IEnumerator IEnumerable.GetEnumerator() 58 | { 59 | return ((IEnumerable)(array ?? [])).GetEnumerator(); 60 | } 61 | 62 | public int Count => array?.Length ?? 0; 63 | 64 | /// 65 | /// Checks whether two values are the same. 66 | /// 67 | /// The first value. 68 | /// The second value. 69 | /// Whether and are equal. 70 | public static bool operator ==(EquatableArray left, EquatableArray right) 71 | { 72 | return left.Equals(right); 73 | } 74 | 75 | /// 76 | /// Checks whether two values are not the same. 77 | /// 78 | /// The first value. 79 | /// The second value. 80 | /// Whether and are not equal. 81 | public static bool operator !=(EquatableArray left, EquatableArray right) 82 | { 83 | return !left.Equals(right); 84 | } 85 | } 86 | 87 | internal static class EquatableArrayBuilder 88 | { 89 | public static EquatableArray Create(ReadOnlySpan values) where T : IEquatable => new(values.ToArray()); 90 | } -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/DiagnosticDescriptors.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace ServiceScan.SourceGenerator; 4 | 5 | public static class DiagnosticDescriptors 6 | { 7 | public static readonly DiagnosticDescriptor NotPartialDefinition = new("DI0001", 8 | "Method is not partial", 9 | "Method with GenerateServiceRegistrations attribute must have partial modifier", 10 | "Usage", 11 | DiagnosticSeverity.Error, 12 | true); 13 | 14 | public static readonly DiagnosticDescriptor WrongReturnType = new("DI0002", 15 | "Wrong return type", 16 | "Method with GenerateServiceRegistrations attribute must return void or IServiceCollection", 17 | "Usage", 18 | DiagnosticSeverity.Error, 19 | true); 20 | 21 | public static readonly DiagnosticDescriptor WrongMethodParameters = new("DI0003", 22 | "Wrong method parameters", 23 | "Method with GenerateServiceRegistrations attribute must have a single IServiceCollection parameter", 24 | "Usage", 25 | DiagnosticSeverity.Error, 26 | true); 27 | 28 | public static readonly DiagnosticDescriptor MissingSearchCriteria = new("DI0004", 29 | "Missing search criteria", 30 | "GenerateServiceRegistrations must have at least one search criteria", 31 | "Usage", 32 | DiagnosticSeverity.Error, 33 | true); 34 | 35 | public static readonly DiagnosticDescriptor NoMatchingTypesFound = new("DI0005", 36 | "No matching types found", 37 | "There are no types matching attribute's search criteria", 38 | "Usage", 39 | DiagnosticSeverity.Warning, 40 | true); 41 | 42 | public static readonly DiagnosticDescriptor KeySelectorMethodHasIncorrectSignature = new("DI0007", 43 | "Provided KeySelector method has incorrect signature", 44 | "KeySelector should have non-void return type, and either be generic with no parameters, or non-generic with a single Type parameter", 45 | "Usage", 46 | DiagnosticSeverity.Error, 47 | true); 48 | 49 | public static readonly DiagnosticDescriptor CantMixRegularAndCustomHandlerRegistrations = new("DI0008", 50 | "It's not allowed to mix GenerateServiceRegistrations attributes with and without CustomHandler on the same method", 51 | "It's not allowed to mix GenerateServiceRegistrations attributes with and without CustomHandler on the same method", 52 | "Usage", 53 | DiagnosticSeverity.Error, 54 | true); 55 | 56 | public static readonly DiagnosticDescriptor WrongReturnTypeForCustomHandler = new("DI0009", 57 | "Wrong return type", 58 | "Method with CustomHandler must return void or the type of its first parameter", 59 | "Usage", 60 | DiagnosticSeverity.Error, 61 | true); 62 | 63 | public static readonly DiagnosticDescriptor CustomHandlerMethodHasIncorrectSignature = new("DI0011", 64 | "Provided CustomHandler method has incorrect signature", 65 | "CustomHandler method must be generic, and must have the same parameters as the method with the attribute", 66 | "Usage", 67 | DiagnosticSeverity.Error, 68 | true); 69 | 70 | public static readonly DiagnosticDescriptor CantUseBothFromAssemblyOfAndAssemblyNameFilter = new("DI0012", 71 | "Only one assembly selection criteria allowed", 72 | "It is not allowed to use both FromAssemblyOf and AssemblyNameFilter in the same attribute", 73 | "Usage", 74 | DiagnosticSeverity.Error, 75 | true); 76 | } 77 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/DependencyInjectionGenerator.ParseMethodModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.CodeAnalysis; 4 | using ServiceScan.SourceGenerator.Extensions; 5 | using ServiceScan.SourceGenerator.Model; 6 | using static ServiceScan.SourceGenerator.DiagnosticDescriptors; 7 | 8 | namespace ServiceScan.SourceGenerator; 9 | 10 | public partial class DependencyInjectionGenerator 11 | { 12 | private static DiagnosticModel? ParseRegisterMethodModel(GeneratorAttributeSyntaxContext context) 13 | { 14 | if (context.TargetSymbol is not IMethodSymbol method) 15 | return null; 16 | 17 | if (!method.IsPartialDefinition) 18 | return Diagnostic.Create(NotPartialDefinition, method.Locations[0]); 19 | 20 | var position = context.TargetNode.SpanStart; 21 | var attributeData = context.Attributes.Select(a => AttributeModel.Create(a, method, context.SemanticModel)).ToArray(); 22 | var hasCustomHandlers = attributeData.Any(a => a.CustomHandler != null); 23 | 24 | foreach (var attribute in attributeData) 25 | { 26 | if (!attribute.HasSearchCriteria) 27 | return Diagnostic.Create(MissingSearchCriteria, attribute.Location); 28 | 29 | if (hasCustomHandlers && attribute.CustomHandler == null) 30 | return Diagnostic.Create(CantMixRegularAndCustomHandlerRegistrations, attribute.Location); 31 | 32 | if (attribute.AssemblyOfTypeName != null && attribute.AssemblyNameFilter != null) 33 | return Diagnostic.Create(CantUseBothFromAssemblyOfAndAssemblyNameFilter, attribute.Location); 34 | 35 | if (attribute.KeySelector != null) 36 | { 37 | var keySelectorMethod = method.ContainingType.GetMethod(attribute.KeySelector, context.SemanticModel, position, isStatic: true); 38 | 39 | if (keySelectorMethod is not null) 40 | { 41 | if (keySelectorMethod.ReturnsVoid) 42 | return Diagnostic.Create(KeySelectorMethodHasIncorrectSignature, attribute.Location); 43 | 44 | var validGenericKeySelector = keySelectorMethod.TypeArguments.Length == 1 && keySelectorMethod.Parameters.Length == 0; 45 | var validNonGenericKeySelector = !keySelectorMethod.IsGenericMethod && keySelectorMethod.Parameters is [{ Type.Name: nameof(Type) }]; 46 | 47 | if (!validGenericKeySelector && !validNonGenericKeySelector) 48 | return Diagnostic.Create(KeySelectorMethodHasIncorrectSignature, attribute.Location); 49 | } 50 | } 51 | 52 | if (attribute.CustomHandler != null) 53 | { 54 | var customHandlerMethod = method.ContainingType.GetMethod(attribute.CustomHandler, context.SemanticModel, position); 55 | 56 | if (customHandlerMethod != null) 57 | { 58 | if (!customHandlerMethod.IsGenericMethod) 59 | return Diagnostic.Create(CustomHandlerMethodHasIncorrectSignature, attribute.Location); 60 | 61 | var typesMatch = Enumerable.SequenceEqual( 62 | method.Parameters.Select(p => p.Type), 63 | customHandlerMethod.Parameters.Select(p => p.Type), 64 | SymbolEqualityComparer.Default); 65 | 66 | if (!typesMatch) 67 | return Diagnostic.Create(CustomHandlerMethodHasIncorrectSignature, attribute.Location); 68 | } 69 | } 70 | 71 | if (attribute.HasErrors) 72 | return null; 73 | } 74 | 75 | if (!hasCustomHandlers) 76 | { 77 | var serviceCollectionType = context.SemanticModel.Compilation.GetTypeByMetadataName("Microsoft.Extensions.DependencyInjection.IServiceCollection"); 78 | 79 | if (!method.ReturnsVoid && !SymbolEqualityComparer.Default.Equals(method.ReturnType, serviceCollectionType)) 80 | return Diagnostic.Create(WrongReturnType, method.Locations[0]); 81 | 82 | if (method.Parameters.Length != 1 || !SymbolEqualityComparer.Default.Equals(method.Parameters[0].Type, serviceCollectionType)) 83 | return Diagnostic.Create(WrongMethodParameters, method.Locations[0]); 84 | } 85 | else 86 | { 87 | if (!method.ReturnsVoid && 88 | (method.Parameters.Length == 0 || !SymbolEqualityComparer.Default.Equals(method.Parameters[0].Type, method.ReturnType))) 89 | { 90 | return Diagnostic.Create(WrongReturnTypeForCustomHandler, method.Locations[0]); 91 | } 92 | } 93 | 94 | var model = MethodModel.Create(method, context.TargetNode); 95 | return new MethodWithAttributesModel(model, [.. attributeData]); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/GenerateAttributeInfo.cs: -------------------------------------------------------------------------------- 1 | namespace ServiceScan.SourceGenerator; 2 | 3 | internal static class GenerateAttributeInfo 4 | { 5 | public const string MetadataName = "ServiceScan.SourceGenerator.GenerateServiceRegistrationsAttribute"; 6 | 7 | public const string Source = """ 8 | #nullable enable 9 | 10 | using System; 11 | using System.Diagnostics; 12 | using Microsoft.Extensions.DependencyInjection; 13 | 14 | namespace ServiceScan.SourceGenerator; 15 | 16 | [Conditional("CODE_ANALYSIS")] 17 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] 18 | internal class GenerateServiceRegistrationsAttribute : Attribute 19 | { 20 | /// 21 | /// Sets the assembly containing the given type as the source of types to register. 22 | /// If not specified, the assembly containing the method with this attribute will be used. 23 | /// 24 | public Type? FromAssemblyOf { get; set; } 25 | 26 | /// 27 | /// Sets this value to filter scanned assemblies by assembly name. 28 | /// It allows applying an attribute to multiple assemblies. 29 | /// For example, this allows scanning all assemblies from your solution. 30 | /// This option is incompatible with . 31 | /// You can use '*' wildcards. You can also use ',' to separate multiple filters. 32 | /// 33 | /// Be careful to include a limited number of assemblies, as it can affect build and editor performance. 34 | /// My.Product.* 35 | public string? AssemblyNameFilter { get; set; } 36 | 37 | /// 38 | /// Sets the type that the registered types must be assignable to. 39 | /// Types will be registered with this type as the service type, 40 | /// unless or is set. 41 | /// 42 | public Type? AssignableTo { get; set; } 43 | 44 | /// 45 | /// Sets the type that the registered types must *not* be assignable to. 46 | /// 47 | public Type? ExcludeAssignableTo { get; set; } 48 | 49 | /// 50 | /// Sets the lifetime of the registered services. 51 | /// is used if not specified. 52 | /// 53 | public ServiceLifetime Lifetime { get; set; } 54 | 55 | /// 56 | /// If set to true, types will be registered as their implemented interfaces instead of their actual type. 57 | /// 58 | public bool AsImplementedInterfaces { get; set; } 59 | 60 | /// 61 | /// If set to true, types will be registered with their actual type. 62 | /// It can be combined with . In this case, implemented interfaces will be 63 | /// "forwarded" to the "self" implementation. 64 | /// 65 | public bool AsSelf { get; set; } 66 | 67 | /// 68 | /// Sets this value to filter the types to register by their full name. 69 | /// You can use '*' wildcards. 70 | /// You can also use ',' to separate multiple filters. 71 | /// 72 | /// Namespace.With.Services.* 73 | /// *Service,*Factory 74 | public string? TypeNameFilter { get; set; } 75 | 76 | /// 77 | /// Filters types by the specified attribute type being present. 78 | /// 79 | public Type? AttributeFilter { get; set; } 80 | 81 | /// 82 | /// Sets this value to exclude types from being registered by their full name. 83 | /// You can use '*' wildcards. 84 | /// You can also use ',' to separate multiple filters. 85 | /// 86 | /// Namespace.With.Services.* 87 | /// *Service,*Factory 88 | public string? ExcludeByTypeName { get; set; } 89 | 90 | /// 91 | /// Excludes matching types by the specified attribute type being present. 92 | /// 93 | public Type? ExcludeByAttribute { get; set; } 94 | 95 | /// 96 | /// Sets this property to add types as keyed services. 97 | /// This property should point to one of the following: 98 | /// - The name of a static method in the current type with a string return type. 99 | /// The method should be either generic or have a single parameter of type . 100 | /// - A constant field or static property in the implementation type. 101 | /// 102 | /// nameof(GetKey) 103 | public string? KeySelector { get; set; } 104 | 105 | /// 106 | /// Sets this property to invoke a custom method for each type found instead of regular registration logic. 107 | /// This property should point to one of the following: 108 | /// - Name of a generic method in the current type. 109 | /// - Static method name in found types. 110 | /// This property is incompatible with , , , 111 | /// and properties. 112 | /// 113 | public string? CustomHandler { get; set; } 114 | } 115 | """; 116 | } 117 | 118 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/DependencyInjectionGenerator.FindServicesToRegister.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.CodeAnalysis; 4 | using ServiceScan.SourceGenerator.Model; 5 | using static ServiceScan.SourceGenerator.DiagnosticDescriptors; 6 | 7 | namespace ServiceScan.SourceGenerator; 8 | 9 | public partial class DependencyInjectionGenerator 10 | { 11 | private static readonly string[] ExcludedInterfaces = [ 12 | "System.IDisposable", 13 | "System.IAsyncDisposable" 14 | ]; 15 | 16 | private static DiagnosticModel FindServicesToRegister((DiagnosticModel, Compilation) context) 17 | { 18 | var (diagnosticModel, compilation) = context; 19 | var diagnostic = diagnosticModel.Diagnostic; 20 | 21 | if (diagnostic != null) 22 | return diagnostic; 23 | 24 | var (method, attributes) = diagnosticModel.Model; 25 | 26 | var containingType = compilation.GetTypeByMetadataName(method.TypeMetadataName); 27 | var registrations = new List(); 28 | var customHandlers = new List(); 29 | 30 | foreach (var attribute in attributes) 31 | { 32 | bool typesFound = false; 33 | 34 | foreach (var (implementationType, matchedTypes) in FilterTypes(compilation, attribute, containingType)) 35 | { 36 | typesFound = true; 37 | 38 | if (attribute.CustomHandler != null) 39 | { 40 | var implementationTypeName = implementationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); 41 | 42 | // If CustomHandler method has multiple type parameters, which are resolvable from the first one - we try to provide them. 43 | // e.g. ApplyConfiguration(ModelBuilder modelBuilder) where T : IEntityTypeConfiguration 44 | if (attribute.CustomHandlerMethodTypeParametersCount > 1 && matchedTypes != null) 45 | { 46 | foreach (var matchedType in matchedTypes) 47 | { 48 | EquatableArray typeArguments = 49 | [ 50 | implementationTypeName, 51 | .. matchedType.TypeArguments.Select(a => a.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)) 52 | ]; 53 | 54 | customHandlers.Add(new CustomHandlerModel( 55 | attribute.CustomHandlerType.Value, 56 | attribute.CustomHandler, 57 | implementationTypeName, 58 | typeArguments)); 59 | } 60 | } 61 | else 62 | { 63 | customHandlers.Add(new CustomHandlerModel( 64 | attribute.CustomHandlerType.Value, 65 | attribute.CustomHandler, 66 | implementationTypeName, 67 | [implementationTypeName])); 68 | } 69 | } 70 | else 71 | { 72 | var serviceTypes = (attribute.AsSelf, attribute.AsImplementedInterfaces) switch 73 | { 74 | (true, true) => [implementationType, .. GetSuitableInterfaces(implementationType)], 75 | (false, true) => GetSuitableInterfaces(implementationType), 76 | (true, false) => [implementationType], 77 | _ => matchedTypes ?? [implementationType] 78 | }; 79 | 80 | foreach (var serviceType in serviceTypes) 81 | { 82 | if (implementationType.IsGenericType) 83 | { 84 | var implementationTypeName = implementationType.ConstructUnboundGenericType().ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); 85 | var serviceTypeName = serviceType.IsGenericType 86 | ? serviceType.ConstructUnboundGenericType().ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) 87 | : serviceType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); 88 | 89 | var registration = new ServiceRegistrationModel( 90 | attribute.Lifetime, 91 | serviceTypeName, 92 | implementationTypeName, 93 | ResolveImplementation: false, 94 | IsOpenGeneric: true, 95 | attribute.KeySelector, 96 | attribute.KeySelectorType); 97 | 98 | registrations.Add(registration); 99 | } 100 | else 101 | { 102 | var shouldResolve = attribute.AsSelf && attribute.AsImplementedInterfaces && !SymbolEqualityComparer.Default.Equals(implementationType, serviceType); 103 | var registration = new ServiceRegistrationModel( 104 | attribute.Lifetime, 105 | serviceType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), 106 | implementationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), 107 | shouldResolve, 108 | IsOpenGeneric: false, 109 | attribute.KeySelector, 110 | attribute.KeySelectorType); 111 | 112 | registrations.Add(registration); 113 | } 114 | } 115 | } 116 | } 117 | 118 | if (!typesFound) 119 | diagnostic ??= Diagnostic.Create(NoMatchingTypesFound, attribute.Location); 120 | } 121 | 122 | var implementationModel = new MethodImplementationModel(method, [.. registrations], [.. customHandlers]); 123 | return new(diagnostic, implementationModel); 124 | } 125 | 126 | private static IEnumerable GetSuitableInterfaces(ITypeSymbol type) 127 | { 128 | return type.AllInterfaces.Where(x => !ExcludedInterfaces.Contains(x.ToDisplayString())); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/Model/AttributeModel.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.CodeAnalysis; 3 | using ServiceScan.SourceGenerator.Extensions; 4 | 5 | namespace ServiceScan.SourceGenerator.Model; 6 | 7 | enum KeySelectorType { Method, GenericMethod, TypeMember }; 8 | enum CustomHandlerType { Method, TypeMethod }; 9 | 10 | record AttributeModel( 11 | string? AssignableToTypeName, 12 | int AssignableToTypeParametersCount, 13 | string? AssemblyNameFilter, 14 | EquatableArray? AssignableToGenericArguments, 15 | string? AssemblyOfTypeName, 16 | string Lifetime, 17 | string? AttributeFilterTypeName, 18 | string? TypeNameFilter, 19 | string? ExcludeByAttributeTypeName, 20 | string? ExcludeByTypeName, 21 | string? ExcludeAssignableToTypeName, 22 | EquatableArray? ExcludeAssignableToGenericArguments, 23 | string? KeySelector, 24 | KeySelectorType? KeySelectorType, 25 | string? CustomHandler, 26 | CustomHandlerType? CustomHandlerType, 27 | int CustomHandlerMethodTypeParametersCount, 28 | bool AsImplementedInterfaces, 29 | bool AsSelf, 30 | Location Location, 31 | bool HasErrors) 32 | { 33 | public bool HasSearchCriteria => TypeNameFilter != null || AssignableToTypeName != null || AttributeFilterTypeName != null; 34 | 35 | public static AttributeModel Create(AttributeData attribute, IMethodSymbol method, SemanticModel semanticModel) 36 | { 37 | var position = attribute.ApplicationSyntaxReference?.Span.Start ?? 0; 38 | 39 | var assemblyType = attribute.NamedArguments.FirstOrDefault(a => a.Key == "FromAssemblyOf").Value.Value as INamedTypeSymbol; 40 | var assemblyNameFilter = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AssemblyNameFilter").Value.Value as string; 41 | var assignableTo = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AssignableTo").Value.Value as INamedTypeSymbol; 42 | var asImplementedInterfaces = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AsImplementedInterfaces").Value.Value is true; 43 | var asSelf = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AsSelf").Value.Value is true; 44 | var attributeFilterType = attribute.NamedArguments.FirstOrDefault(a => a.Key == "AttributeFilter").Value.Value as INamedTypeSymbol; 45 | var typeNameFilter = attribute.NamedArguments.FirstOrDefault(a => a.Key == "TypeNameFilter").Value.Value as string; 46 | var excludeByAttributeType = attribute.NamedArguments.FirstOrDefault(a => a.Key == "ExcludeByAttribute").Value.Value as INamedTypeSymbol; 47 | var excludeByTypeName = attribute.NamedArguments.FirstOrDefault(a => a.Key == "ExcludeByTypeName").Value.Value as string; 48 | var excludeAssignableTo = attribute.NamedArguments.FirstOrDefault(a => a.Key == "ExcludeAssignableTo").Value.Value as INamedTypeSymbol; 49 | var keySelector = attribute.NamedArguments.FirstOrDefault(a => a.Key == "KeySelector").Value.Value as string; 50 | var customHandler = attribute.NamedArguments.FirstOrDefault(a => a.Key == "CustomHandler").Value.Value as string; 51 | 52 | var assignableToTypeParametersCount = assignableTo?.TypeParameters.Length ?? 0; 53 | 54 | KeySelectorType? keySelectorType = null; 55 | if (keySelector != null) 56 | { 57 | var keySelectorMethod = method.ContainingType.GetMethod(keySelector, semanticModel, position, isStatic: true); 58 | 59 | if (keySelectorMethod != null) 60 | { 61 | keySelectorType = keySelectorMethod.IsGenericMethod ? Model.KeySelectorType.GenericMethod : Model.KeySelectorType.Method; 62 | } 63 | else 64 | { 65 | keySelectorType = Model.KeySelectorType.TypeMember; 66 | } 67 | } 68 | 69 | CustomHandlerType? customHandlerType = null; 70 | var customHandlerGenericParameters = 0; 71 | if (customHandler != null) 72 | { 73 | var customHandlerMethod = method.ContainingType.GetMethod(customHandler, semanticModel, position); 74 | 75 | customHandlerType = customHandlerMethod != null ? Model.CustomHandlerType.Method : Model.CustomHandlerType.TypeMethod; 76 | customHandlerGenericParameters = customHandlerMethod?.TypeParameters.Length ?? 0; 77 | } 78 | 79 | if (string.IsNullOrWhiteSpace(typeNameFilter)) 80 | typeNameFilter = null; 81 | 82 | if (string.IsNullOrWhiteSpace(excludeByTypeName)) 83 | excludeByTypeName = null; 84 | 85 | if (string.IsNullOrWhiteSpace(assemblyNameFilter)) 86 | assemblyNameFilter = null; 87 | 88 | var attributeFilterTypeName = attributeFilterType?.ToFullMetadataName(); 89 | var excludeByAttributeTypeName = excludeByAttributeType?.ToFullMetadataName(); 90 | var assemblyOfTypeName = assemblyType?.ToFullMetadataName(); 91 | var assignableToTypeName = assignableTo?.ToFullMetadataName(); 92 | var excludeAssignableToTypeName = excludeAssignableTo?.ToFullMetadataName(); 93 | EquatableArray? assignableToGenericArguments = assignableTo != null && assignableTo.IsGenericType && !assignableTo.IsUnboundGenericType 94 | ? [.. assignableTo?.TypeArguments.Select(t => t.ToFullMetadataName())] 95 | : null; 96 | EquatableArray? excludeAssignableToGenericArguments = excludeAssignableTo != null && excludeAssignableTo.IsGenericType && !excludeAssignableTo.IsUnboundGenericType 97 | ? [.. excludeAssignableTo?.TypeArguments.Select(t => t.ToFullMetadataName())] 98 | : null; 99 | 100 | var lifetime = (attribute.NamedArguments.FirstOrDefault(a => a.Key == "Lifetime").Value.Value as int?) switch 101 | { 102 | 0 => "Singleton", 103 | 1 => "Scoped", 104 | _ => "Transient" 105 | }; 106 | 107 | var syntax = attribute.ApplicationSyntaxReference.SyntaxTree; 108 | var textSpan = attribute.ApplicationSyntaxReference.Span; 109 | var location = Location.Create(syntax, textSpan); 110 | 111 | var hasError = assemblyType is { TypeKind: TypeKind.Error } 112 | || assignableTo is { TypeKind: TypeKind.Error } 113 | || attributeFilterType is { TypeKind: TypeKind.Error }; 114 | 115 | return new( 116 | assignableToTypeName, 117 | assignableToTypeParametersCount, 118 | assemblyNameFilter, 119 | assignableToGenericArguments, 120 | assemblyOfTypeName, 121 | lifetime, 122 | attributeFilterTypeName, 123 | typeNameFilter, 124 | excludeByAttributeTypeName, 125 | excludeByTypeName, 126 | excludeAssignableToTypeName, 127 | excludeAssignableToGenericArguments, 128 | keySelector, 129 | keySelectorType, 130 | customHandler, 131 | customHandlerType, 132 | customHandlerGenericParameters, 133 | asImplementedInterfaces, 134 | asSelf, 135 | location, 136 | hasError); 137 | } 138 | } -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/DependencyInjectionGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Text; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; 5 | using Microsoft.CodeAnalysis.Text; 6 | using ServiceScan.SourceGenerator.Extensions; 7 | using ServiceScan.SourceGenerator.Model; 8 | 9 | namespace ServiceScan.SourceGenerator; 10 | 11 | [Generator] 12 | public partial class DependencyInjectionGenerator : IIncrementalGenerator 13 | { 14 | public void Initialize(IncrementalGeneratorInitializationContext context) 15 | { 16 | context.RegisterPostInitializationOutput(context => 17 | { 18 | context.AddSource("ServiceScanAttributes.Generated.cs", SourceText.From(GenerateAttributeInfo.Source, Encoding.UTF8)); 19 | }); 20 | 21 | var methodProvider = context.SyntaxProvider.ForAttributeWithMetadataName( 22 | GenerateAttributeInfo.MetadataName, 23 | predicate: static (syntaxNode, ct) => syntaxNode is MethodDeclarationSyntax methodSyntax, 24 | transform: static (context, ct) => ParseRegisterMethodModel(context)) 25 | .Where(method => method != null); 26 | 27 | var combinedProvider = methodProvider.Combine(context.CompilationProvider) 28 | .WithComparer(CombinedProviderComparer.Instance); 29 | 30 | var methodImplementationsProvider = combinedProvider 31 | .Select(static (context, ct) => FindServicesToRegister(context)); 32 | 33 | context.RegisterSourceOutput(methodImplementationsProvider, 34 | static (context, src) => 35 | { 36 | if (src.Diagnostic != null) 37 | context.ReportDiagnostic(src.Diagnostic); 38 | 39 | if (src.Model == null) 40 | return; 41 | 42 | var (method, registrations, customHandling) = src.Model; 43 | string source = registrations.Count > 0 44 | ? GenerateRegistrationsSource(method, registrations) 45 | : GenerateCustomHandlingSource(method, customHandling); 46 | 47 | source = source.ReplaceLineEndings(); 48 | 49 | context.AddSource($"{method.TypeName}_{method.MethodName}.Generated.cs", SourceText.From(source, Encoding.UTF8)); 50 | }); 51 | } 52 | 53 | private static string GenerateRegistrationsSource(MethodModel method, EquatableArray registrations) 54 | { 55 | var registrationsCode = string.Concat(registrations 56 | .Select(registration => 57 | { 58 | if (registration.IsOpenGeneric) 59 | { 60 | return $".Add{registration.Lifetime}(typeof({registration.ServiceTypeName}), typeof({registration.ImplementationTypeName}))"; 61 | } 62 | else 63 | { 64 | if (registration.ResolveImplementation) 65 | { 66 | return $".Add{registration.Lifetime}<{registration.ServiceTypeName}>(s => s.GetRequiredService<{registration.ImplementationTypeName}>())"; 67 | } 68 | else 69 | { 70 | var addMethod = registration.KeySelector != null 71 | ? $"AddKeyed{registration.Lifetime}" 72 | : $"Add{registration.Lifetime}"; 73 | 74 | var keySelectorInvocation = registration.KeySelectorType switch 75 | { 76 | KeySelectorType.GenericMethod => $"{registration.KeySelector}<{registration.ImplementationTypeName}>()", 77 | KeySelectorType.Method => $"{registration.KeySelector}(typeof({registration.ImplementationTypeName}))", 78 | KeySelectorType.TypeMember => $"{registration.ImplementationTypeName}.{registration.KeySelector}", 79 | _ => null 80 | }; 81 | 82 | return $".{addMethod}<{registration.ServiceTypeName}, {registration.ImplementationTypeName}>({keySelectorInvocation})"; 83 | } 84 | } 85 | }) 86 | .Select(line => $"\n {line}")); 87 | 88 | var returnType = method.ReturnsVoid ? "void" : "IServiceCollection"; 89 | var namespaceDeclaration = method.Namespace is null ? "" : $"namespace {method.Namespace};"; 90 | 91 | var methodBody = registrations.Count == 0 && method.ReturnsVoid 92 | ? "" 93 | : $$"""{{(method.ReturnsVoid ? "" : "return ")}}{{method.ParameterName}}{{registrationsCode}};"""; 94 | 95 | var source = $$""" 96 | using Microsoft.Extensions.DependencyInjection; 97 | 98 | {{namespaceDeclaration}} 99 | 100 | {{method.TypeModifiers}} class {{method.TypeName}} 101 | { 102 | {{method.MethodModifiers}} {{returnType}} {{method.MethodName}}({{(method.IsExtensionMethod ? "this" : "")}} IServiceCollection {{method.ParameterName}}) 103 | { 104 | {{methodBody}} 105 | } 106 | } 107 | """; 108 | 109 | return source; 110 | } 111 | 112 | private static string GenerateCustomHandlingSource(MethodModel method, EquatableArray customHandlers) 113 | { 114 | var invocations = string.Join("\n", customHandlers.Select(h => 115 | { 116 | if (h.CustomHandlerType == CustomHandlerType.Method) 117 | { 118 | var genericArguments = string.Join(", ", h.TypeArguments); 119 | var arguments = string.Join(", ", method.Parameters.Select(p => p.Name)); 120 | return $" {h.HandlerMethodName}<{genericArguments}>({arguments});"; 121 | } 122 | else 123 | { 124 | var arguments = string.Join(", ", method.Parameters.Select(p => p.Name)); 125 | return $" {h.TypeName}.{h.HandlerMethodName}({arguments});"; 126 | } 127 | })); 128 | 129 | var namespaceDeclaration = method.Namespace is null ? "" : $"namespace {method.Namespace};"; 130 | var parameters = string.Join(",", method.Parameters.Select((p, i) => 131 | $"{(i == 0 && method.IsExtensionMethod ? "this" : "")} {p.Type} {p.Name}")); 132 | 133 | var methodBody = $$""" 134 | {{invocations.Trim()}} 135 | {{(method.ReturnsVoid ? "" : $"return {method.ParameterName};")}} 136 | """; 137 | 138 | var source = $$""" 139 | {{namespaceDeclaration}} 140 | 141 | {{method.TypeModifiers}} class {{method.TypeName}} 142 | { 143 | {{method.MethodModifiers}} {{method.ReturnType}} {{method.MethodName}}({{parameters}}) 144 | { 145 | {{methodBody.Trim()}} 146 | } 147 | } 148 | """; 149 | 150 | return source; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd 364 | 365 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ServiceScan.SourceGenerator 2 | [![NuGet Version](https://img.shields.io/nuget/v/ServiceScan.SourceGenerator)](https://www.nuget.org/packages/ServiceScan.SourceGenerator/) 3 | [![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua) 4 | 5 | Source generator for services registrations inspired by [Scrutor](https://github.com/khellang/Scrutor/). 6 | Code generation allows to have AOT-compatible code, without an additional hit on startup performance due to runtime assembly scanning. 7 | 8 | ## Installation 9 | Add the NuGet Package to your project: 10 | ``` 11 | dotnet add package ServiceScan.SourceGenerator 12 | ``` 13 | 14 | ## Usage 15 | 16 | `ServiceScan` generates a partial method implementation based on `GenerateServiceRegistrations` attribute. This attribute can be added to a partial method with `IServiceCollection` parameter. 17 | For example, based on the following partial method: 18 | ```csharp 19 | public static partial class ServicesExtensions 20 | { 21 | [GenerateServiceRegistrations(AssignableTo = typeof(IMyService), Lifetime = ServiceLifetime.Scoped)] 22 | public static partial IServiceCollection AddServices(this IServiceCollection services); 23 | } 24 | ``` 25 | 26 | `ServiceScan` will generate the following implementation: 27 | ```csharp 28 | public static partial class ServicesExtensions 29 | { 30 | public static partial IServiceCollection AddServices(this IServiceCollection services) 31 | { 32 | return services 33 | .AddScoped() 34 | .AddScoped(); 35 | } 36 | } 37 | ``` 38 | 39 | The only thing left is to invoke this method on your `IServiceCollection` instance 40 | ```csharp 41 | services.AddServices(); 42 | ``` 43 | 44 | ## Examples 45 | 46 | ### Register all [FluentValidation](https://github.com/FluentValidation/FluentValidation) validators 47 | Unlike using `FluentValidation.DependencyInjectionExtensions` package, `ServiceScan` is AOT-compatible, and doesn't affect startup performance: 48 | ```csharp 49 | [GenerateServiceRegistrations(AssignableTo = typeof(IValidator<>), Lifetime = ServiceLifetime.Singleton)] 50 | public static partial IServiceCollection AddValidators(this IServiceCollection services); 51 | ``` 52 | 53 | ### Add [MediatR](https://github.com/jbogard/MediatR) handlers 54 | ```csharp 55 | public static IServiceCollection AddMediatR(this IServiceCollection services) 56 | { 57 | return services 58 | .AddTransient() 59 | .AddMediatRHandlers(); 60 | } 61 | 62 | [GenerateServiceRegistrations(AssignableTo = typeof(IRequestHandler<>), Lifetime = ServiceLifetime.Transient)] 63 | [GenerateServiceRegistrations(AssignableTo = typeof(IRequestHandler<,>), Lifetime = ServiceLifetime.Transient)] 64 | private static partial IServiceCollection AddMediatRHandlers(this IServiceCollection services); 65 | ``` 66 | It adds MediatR requests handlers, although you might need to add other types like PipelineBehaviors or NotificationHandlers. 67 | 68 | ### Add all repository types from your project based on name filter as their implemented interfaces: 69 | ```csharp 70 | [GenerateServiceRegistrations( 71 | TypeNameFilter = "*Repository", 72 | AsImplementedInterfaces = true, 73 | Lifetime = ServiceLifetime.Scoped)] 74 | private static partial IServiceCollection AddRepositories(this IServiceCollection services); 75 | ``` 76 | 77 | ### Add AspNetCore Minimal API endpoints 78 | You can add custom type handler, if you need to do something non-trivial with that type. For example, you can automatically discover 79 | and map Minimal API endpoints: 80 | ```csharp 81 | public interface IEndpoint 82 | { 83 | abstract static void MapEndpoint(IEndpointRouteBuilder endpoints); 84 | } 85 | 86 | public class HelloWorldEndpoint : IEndpoint 87 | { 88 | public static void MapEndpoint(IEndpointRouteBuilder endpoints) 89 | { 90 | endpoints.MapGet("/", () => "Hello World!"); 91 | } 92 | } 93 | 94 | public static partial class ServiceCollectionExtensions 95 | { 96 | [GenerateServiceRegistrations(AssignableTo = typeof(IEndpoint), CustomHandler = nameof(IEndpoint.MapEndpoint))] 97 | public static partial IEndpointRouteBuilder MapEndpoints(this IEndpointRouteBuilder endpoints); 98 | } 99 | ``` 100 | 101 | ### Register Options types 102 | Another example of `CustomHandler` is to register Options types. We can define custom `OptionAttribute`, which allows to specify configuration section key. 103 | And then read that value in our `CustomHandler`: 104 | ```csharp 105 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] 106 | public class OptionAttribute(string? section = null) : Attribute 107 | { 108 | public string? Section { get; } = section; 109 | } 110 | 111 | [Option] 112 | public record RootSection { } 113 | 114 | [Option("SectionOption")] 115 | public record SectionOption { } 116 | 117 | public static partial class ServiceCollectionExtensions 118 | { 119 | [GenerateServiceRegistrations(AttributeFilter = typeof(OptionAttribute), CustomHandler = nameof(AddOption))] 120 | public static partial IServiceCollection AddOptions(this IServiceCollection services, IConfiguration configuration); 121 | 122 | private static void AddOption(IServiceCollection services, IConfiguration configuration) where T : class 123 | { 124 | var sectionKey = typeof(T).GetCustomAttribute()?.Section; 125 | var section = sectionKey is null ? configuration : configuration.GetSection(sectionKey); 126 | services.Configure(section); 127 | } 128 | } 129 | ``` 130 | 131 | ### Apply EF Core IEntityTypeConfiguration types 132 | 133 | ```csharp 134 | public static partial class ModelBuilderExtensions 135 | { 136 | [GenerateServiceRegistrations(AssignableTo = typeof(IEntityTypeConfiguration<>), CustomHandler = nameof(ApplyConfiguration))] 137 | public static partial ModelBuilder ApplyEntityConfigurations(this ModelBuilder modelBuilder); 138 | 139 | private static void ApplyConfiguration(ModelBuilder modelBuilder) 140 | where T : IEntityTypeConfiguration, new() 141 | where TEntity : class 142 | { 143 | modelBuilder.ApplyConfiguration(new T()); 144 | } 145 | } 146 | ``` 147 | 148 | 149 | 150 | ## Parameters 151 | 152 | `GenerateServiceRegistrations` attribute has the following properties: 153 | | Property | Description | 154 | | --- | --- | 155 | | **FromAssemblyOf** | Sets the assembly containing the given type as the source of types to register. If not specified, the assembly containing the method with this attribute will be used. | 156 | | **AssemblyNameFilter** | Sets this value to filter scanned assemblies by assembly name. It allows applying an attribute to multiple assemblies. For example, this allows scanning all assemblies from your solution. This option is incompatible with `FromAssemblyOf`. You can use '*' wildcards. You can also use ',' to separate multiple filters. *Be careful to include a limited number of assemblies, as it can affect build and editor performance.* | 157 | | **AssignableTo** | Sets the type that the registered types must be assignable to. Types will be registered with this type as the service type, unless `AsImplementedInterfaces` or `AsSelf` is set. | 158 | | **ExcludeAssignableTo** | Sets the type that the registered types must *not* be assignable to. | 159 | | **Lifetime** | Sets the lifetime of the registered services. `ServiceLifetime.Transient` is used if not specified. | 160 | | **AsImplementedInterfaces** | If set to true, types will be registered as their implemented interfaces instead of their actual type. | 161 | | **AsSelf** | If set to true, types will be registered with their actual type. It can be combined with `AsImplementedInterfaces`. In this case, implemented interfaces will be "forwarded" to the "self" implementation. | 162 | | **TypeNameFilter** | Sets this value to filter the types to register by their full name. You can use '*' wildcards. You can also use ',' to separate multiple filters. | 163 | | **AttributeFilter** | Filters types by the specified attribute type being present. | 164 | | **ExcludeByTypeName** | Sets this value to exclude types from being registered by their full name. You can use '*' wildcards. You can also use ',' to separate multiple filters. | 165 | | **ExcludeByAttribute** | Excludes matching types by the specified attribute type being present. | 166 | | **KeySelector** | Sets this property to add types as keyed services. This property should point to one of the following:
- The name of a static method in the current type with a string return type. The method should be either generic or have a single parameter of type `Type`.
- A constant field or static property in the implementation type. | 167 | | **CustomHandler** | Sets this property to invoke a custom method for each type found instead of regular registration logic. This property should point to one of the following:
- Name of a generic method in the current type.
- Static method name in found types.
This property is incompatible with `Lifetime`, `AsImplementedInterfaces`, `AsSelf`, and `KeySelector` properties.
**Note:** When using a generic `CustomHandler` method, types are automatically filtered by the generic constraints defined on the method's type parameters (e.g., `class`, `struct`, `new()`, interface constraints). | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories 2 | root = true 3 | 4 | # C# files 5 | [*.cs] 6 | 7 | #### Core EditorConfig Options #### 8 | 9 | # Indentation and spacing 10 | indent_size = 4 11 | indent_style = space 12 | tab_width = 4 13 | 14 | # New line preferences 15 | end_of_line = crlf 16 | insert_final_newline = false 17 | 18 | #### .NET Coding Conventions #### 19 | 20 | # Organize usings 21 | dotnet_separate_import_directive_groups = false 22 | dotnet_sort_system_directives_first = true 23 | file_header_template = unset 24 | 25 | # this. and Me. preferences 26 | dotnet_style_qualification_for_event = false 27 | dotnet_style_qualification_for_field = false 28 | dotnet_style_qualification_for_method = false 29 | dotnet_style_qualification_for_property = false 30 | 31 | # Language keywords vs BCL types preferences 32 | dotnet_style_predefined_type_for_locals_parameters_members = true 33 | dotnet_style_predefined_type_for_member_access = true 34 | 35 | # Parentheses preferences 36 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity 37 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity 38 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary 39 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity 40 | 41 | # Modifier preferences 42 | dotnet_style_require_accessibility_modifiers = for_non_interface_members 43 | 44 | # Expression-level preferences 45 | dotnet_style_coalesce_expression = true 46 | dotnet_style_collection_initializer = true 47 | dotnet_style_explicit_tuple_names = true 48 | dotnet_style_namespace_match_folder = true 49 | dotnet_style_null_propagation = true 50 | dotnet_style_object_initializer = true 51 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 52 | dotnet_style_prefer_auto_properties = true 53 | dotnet_style_prefer_collection_expression = when_types_loosely_match 54 | dotnet_style_prefer_compound_assignment = true 55 | dotnet_style_prefer_conditional_expression_over_assignment = true 56 | dotnet_style_prefer_conditional_expression_over_return = true 57 | dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed 58 | dotnet_style_prefer_inferred_anonymous_type_member_names = true 59 | dotnet_style_prefer_inferred_tuple_names = true 60 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true 61 | dotnet_style_prefer_simplified_boolean_expressions = true 62 | dotnet_style_prefer_simplified_interpolation = true 63 | 64 | # Field preferences 65 | dotnet_style_readonly_field = true 66 | 67 | # Parameter preferences 68 | dotnet_code_quality_unused_parameters = all 69 | 70 | # Suppression preferences 71 | dotnet_remove_unnecessary_suppression_exclusions = none 72 | 73 | # New line preferences 74 | dotnet_style_allow_multiple_blank_lines_experimental = true 75 | dotnet_style_allow_statement_immediately_after_block_experimental = true 76 | 77 | #### C# Coding Conventions #### 78 | 79 | # var preferences 80 | csharp_style_var_elsewhere = true 81 | csharp_style_var_for_built_in_types = true 82 | csharp_style_var_when_type_is_apparent = true 83 | 84 | # Expression-bodied members 85 | csharp_style_expression_bodied_accessors = true 86 | csharp_style_expression_bodied_constructors = false 87 | csharp_style_expression_bodied_indexers = true 88 | csharp_style_expression_bodied_lambdas = true 89 | csharp_style_expression_bodied_local_functions = false 90 | csharp_style_expression_bodied_methods = false 91 | csharp_style_expression_bodied_operators = false 92 | csharp_style_expression_bodied_properties = true 93 | 94 | # Pattern matching preferences 95 | csharp_style_pattern_matching_over_as_with_null_check = true 96 | csharp_style_pattern_matching_over_is_with_cast_check = true 97 | csharp_style_prefer_extended_property_pattern = true 98 | csharp_style_prefer_not_pattern = true 99 | csharp_style_prefer_pattern_matching = true 100 | csharp_style_prefer_switch_expression = true 101 | 102 | # Null-checking preferences 103 | csharp_style_conditional_delegate_call = true 104 | 105 | # Modifier preferences 106 | csharp_prefer_static_anonymous_function = true 107 | csharp_prefer_static_local_function = true 108 | csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async 109 | csharp_style_prefer_readonly_struct = true 110 | csharp_style_prefer_readonly_struct_member = true 111 | 112 | # Code-block preferences 113 | csharp_prefer_braces = true 114 | csharp_prefer_simple_using_statement = true 115 | csharp_style_namespace_declarations = file_scoped 116 | csharp_style_prefer_method_group_conversion = true 117 | csharp_style_prefer_primary_constructors = true 118 | csharp_style_prefer_top_level_statements = true 119 | 120 | # Expression-level preferences 121 | csharp_prefer_simple_default_expression = true 122 | csharp_style_deconstructed_variable_declaration = true 123 | csharp_style_implicit_object_creation_when_type_is_apparent = true 124 | csharp_style_inlined_variable_declaration = true 125 | csharp_style_prefer_index_operator = true 126 | csharp_style_prefer_local_over_anonymous_function = true 127 | csharp_style_prefer_null_check_over_type_check = true 128 | csharp_style_prefer_range_operator = true 129 | csharp_style_prefer_tuple_swap = true 130 | csharp_style_prefer_utf8_string_literals = true 131 | csharp_style_throw_expression = true 132 | csharp_style_unused_value_assignment_preference = discard_variable 133 | csharp_style_unused_value_expression_statement_preference = discard_variable 134 | 135 | # 'using' directive preferences 136 | csharp_using_directive_placement = outside_namespace 137 | 138 | # New line preferences 139 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true 140 | csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true 141 | csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true 142 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true 143 | csharp_style_allow_embedded_statements_on_same_line_experimental = true 144 | 145 | #### C# Formatting Rules #### 146 | 147 | # New line preferences 148 | csharp_new_line_before_catch = true 149 | csharp_new_line_before_else = true 150 | csharp_new_line_before_finally = true 151 | csharp_new_line_before_members_in_anonymous_types = true 152 | csharp_new_line_before_members_in_object_initializers = true 153 | csharp_new_line_before_open_brace = all 154 | csharp_new_line_between_query_expression_clauses = true 155 | 156 | # Indentation preferences 157 | csharp_indent_block_contents = true 158 | csharp_indent_braces = false 159 | csharp_indent_case_contents = true 160 | csharp_indent_case_contents_when_block = true 161 | csharp_indent_labels = one_less_than_current 162 | csharp_indent_switch_labels = true 163 | 164 | # Space preferences 165 | csharp_space_after_cast = false 166 | csharp_space_after_colon_in_inheritance_clause = true 167 | csharp_space_after_comma = true 168 | csharp_space_after_dot = false 169 | csharp_space_after_keywords_in_control_flow_statements = true 170 | csharp_space_after_semicolon_in_for_statement = true 171 | csharp_space_around_binary_operators = before_and_after 172 | csharp_space_around_declaration_statements = false 173 | csharp_space_before_colon_in_inheritance_clause = true 174 | csharp_space_before_comma = false 175 | csharp_space_before_dot = false 176 | csharp_space_before_open_square_brackets = false 177 | csharp_space_before_semicolon_in_for_statement = false 178 | csharp_space_between_empty_square_brackets = false 179 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 180 | csharp_space_between_method_call_name_and_opening_parenthesis = false 181 | csharp_space_between_method_call_parameter_list_parentheses = false 182 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 183 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 184 | csharp_space_between_method_declaration_parameter_list_parentheses = false 185 | csharp_space_between_parentheses = false 186 | csharp_space_between_square_brackets = false 187 | 188 | # Wrapping preferences 189 | csharp_preserve_single_line_blocks = true 190 | csharp_preserve_single_line_statements = true 191 | 192 | #### Naming styles #### 193 | 194 | # Naming rules 195 | 196 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 197 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 198 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 199 | 200 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 201 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 202 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 203 | 204 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 205 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 206 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 207 | 208 | # Symbol specifications 209 | 210 | dotnet_naming_symbols.interface.applicable_kinds = interface 211 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 212 | dotnet_naming_symbols.interface.required_modifiers = 213 | 214 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 215 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 216 | dotnet_naming_symbols.types.required_modifiers = 217 | 218 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 219 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 220 | dotnet_naming_symbols.non_field_members.required_modifiers = 221 | 222 | # Naming styles 223 | 224 | dotnet_naming_style.pascal_case.required_prefix = 225 | dotnet_naming_style.pascal_case.required_suffix = 226 | dotnet_naming_style.pascal_case.word_separator = 227 | dotnet_naming_style.pascal_case.capitalization = pascal_case 228 | 229 | dotnet_naming_style.begins_with_i.required_prefix = I 230 | dotnet_naming_style.begins_with_i.required_suffix = 231 | dotnet_naming_style.begins_with_i.word_separator = 232 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 233 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator.Tests/GeneratedMethodTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Xunit; 5 | 6 | namespace ServiceScan.SourceGenerator.Tests; 7 | 8 | public class GeneratedMethodTests 9 | { 10 | private readonly DependencyInjectionGenerator _generator = new(); 11 | 12 | private const string Services = """ 13 | namespace GeneratorTests; 14 | 15 | public interface IService { } 16 | public class MyService : IService { } 17 | """; 18 | 19 | [Theory] 20 | [InlineData("public", "public")] 21 | [InlineData("public", "private")] 22 | [InlineData("internal", "private")] 23 | [InlineData("internal", "public")] 24 | public void StaticExtensionMethodReturningServices(string classAccessModifier, string methodAccessModifier) 25 | { 26 | var compilation = CreateCompilation(Services, 27 | $$""" 28 | using ServiceScan.SourceGenerator; 29 | using Microsoft.Extensions.DependencyInjection; 30 | 31 | namespace GeneratorTests; 32 | 33 | {{classAccessModifier}} static partial class ServicesExtensions 34 | { 35 | [GenerateServiceRegistrations(AssignableTo = typeof(IService))] 36 | {{methodAccessModifier}} static partial IServiceCollection AddServices(this IServiceCollection services); 37 | } 38 | """); 39 | 40 | var results = CSharpGeneratorDriver 41 | .Create(_generator) 42 | .RunGenerators(compilation) 43 | .GetRunResult(); 44 | 45 | var expected = $$""" 46 | using Microsoft.Extensions.DependencyInjection; 47 | 48 | namespace GeneratorTests; 49 | 50 | {{classAccessModifier}} static partial class ServicesExtensions 51 | { 52 | {{methodAccessModifier}} static partial IServiceCollection AddServices(this IServiceCollection services) 53 | { 54 | return services 55 | .AddTransient(); 56 | } 57 | } 58 | """; 59 | 60 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 61 | } 62 | 63 | [Fact] 64 | public void StaticExtensionVoidMethod() 65 | { 66 | var compilation = CreateCompilation(Services, 67 | """ 68 | using ServiceScan.SourceGenerator; 69 | using Microsoft.Extensions.DependencyInjection; 70 | 71 | namespace GeneratorTests; 72 | 73 | public static partial class ServicesExtensions 74 | { 75 | [GenerateServiceRegistrations(AssignableTo = typeof(IService))] 76 | public static partial void AddServices(this IServiceCollection services); 77 | } 78 | """); 79 | 80 | var results = CSharpGeneratorDriver 81 | .Create(_generator) 82 | .RunGenerators(compilation) 83 | .GetRunResult(); 84 | 85 | var expected = """ 86 | using Microsoft.Extensions.DependencyInjection; 87 | 88 | namespace GeneratorTests; 89 | 90 | public static partial class ServicesExtensions 91 | { 92 | public static partial void AddServices(this IServiceCollection services) 93 | { 94 | services 95 | .AddTransient(); 96 | } 97 | } 98 | """; 99 | 100 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 101 | } 102 | 103 | [Fact] 104 | public void StaticMethodReturningServices() 105 | { 106 | var compilation = CreateCompilation(Services, 107 | """ 108 | using ServiceScan.SourceGenerator; 109 | using Microsoft.Extensions.DependencyInjection; 110 | 111 | namespace GeneratorTests; 112 | 113 | public static partial class ServicesExtensions 114 | { 115 | [GenerateServiceRegistrations(AssignableTo = typeof(IService))] 116 | public static partial IServiceCollection AddServices(IServiceCollection services); 117 | } 118 | """); 119 | 120 | var results = CSharpGeneratorDriver 121 | .Create(_generator) 122 | .RunGenerators(compilation) 123 | .GetRunResult(); 124 | 125 | var expected = """ 126 | using Microsoft.Extensions.DependencyInjection; 127 | 128 | namespace GeneratorTests; 129 | 130 | public static partial class ServicesExtensions 131 | { 132 | public static partial IServiceCollection AddServices( IServiceCollection services) 133 | { 134 | return services 135 | .AddTransient(); 136 | } 137 | } 138 | """; 139 | 140 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 141 | } 142 | 143 | [Fact] 144 | public void InstanceVoidMethod() 145 | { 146 | var compilation = CreateCompilation(Services, 147 | """ 148 | using ServiceScan.SourceGenerator; 149 | using Microsoft.Extensions.DependencyInjection; 150 | 151 | namespace GeneratorTests; 152 | 153 | public partial class ServiceType 154 | { 155 | [GenerateServiceRegistrations(AssignableTo = typeof(IService))] 156 | private partial void AddServices(IServiceCollection services); 157 | } 158 | """); 159 | 160 | var results = CSharpGeneratorDriver 161 | .Create(_generator) 162 | .RunGenerators(compilation) 163 | .GetRunResult(); 164 | 165 | var expected = """ 166 | using Microsoft.Extensions.DependencyInjection; 167 | 168 | namespace GeneratorTests; 169 | 170 | public partial class ServiceType 171 | { 172 | private partial void AddServices( IServiceCollection services) 173 | { 174 | services 175 | .AddTransient(); 176 | } 177 | } 178 | """; 179 | 180 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 181 | } 182 | 183 | [Fact] 184 | public void MethodInGlobalNamespace() 185 | { 186 | var compilation = CreateCompilation(Services, 187 | """ 188 | using ServiceScan.SourceGenerator; 189 | using Microsoft.Extensions.DependencyInjection; 190 | using GeneratorTests; 191 | 192 | public static partial class ServicesExtensions 193 | { 194 | [GenerateServiceRegistrations(AssignableTo = typeof(IService))] 195 | public static partial IServiceCollection AddServices(this IServiceCollection services); 196 | } 197 | """); 198 | 199 | var results = CSharpGeneratorDriver 200 | .Create(_generator) 201 | .RunGenerators(compilation) 202 | .GetRunResult(); 203 | 204 | var expected = """ 205 | using Microsoft.Extensions.DependencyInjection; 206 | 207 | 208 | 209 | public static partial class ServicesExtensions 210 | { 211 | public static partial IServiceCollection AddServices(this IServiceCollection services) 212 | { 213 | return services 214 | .AddTransient(); 215 | } 216 | } 217 | """; 218 | 219 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 220 | } 221 | 222 | [Fact] 223 | public void MethodWithCustomParameterName() 224 | { 225 | var compilation = CreateCompilation(Services, 226 | """ 227 | using ServiceScan.SourceGenerator; 228 | using Microsoft.Extensions.DependencyInjection; 229 | 230 | namespace GeneratorTests; 231 | 232 | public static partial class ServicesExtensions 233 | { 234 | [GenerateServiceRegistrations(AssignableTo = typeof(IService))] 235 | public static partial IServiceCollection AddServices(this IServiceCollection strangeServices); 236 | } 237 | """); 238 | 239 | var results = CSharpGeneratorDriver 240 | .Create(_generator) 241 | .RunGenerators(compilation) 242 | .GetRunResult(); 243 | 244 | var expected = """ 245 | using Microsoft.Extensions.DependencyInjection; 246 | 247 | namespace GeneratorTests; 248 | 249 | public static partial class ServicesExtensions 250 | { 251 | public static partial IServiceCollection AddServices(this IServiceCollection strangeServices) 252 | { 253 | return strangeServices 254 | .AddTransient(); 255 | } 256 | } 257 | """; 258 | 259 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 260 | } 261 | 262 | private static Compilation CreateCompilation(params string[] source) 263 | { 264 | var path = Path.GetDirectoryName(typeof(object).Assembly.Location)!; 265 | var runtimeAssemblyPath = Path.Combine(path, "System.Runtime.dll"); 266 | 267 | var runtimeReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); 268 | 269 | return CSharpCompilation.Create("compilation", 270 | source.Select(s => CSharpSyntaxTree.ParseText(s)), 271 | [ 272 | MetadataReference.CreateFromFile(typeof(object).Assembly.Location), 273 | MetadataReference.CreateFromFile(runtimeAssemblyPath), 274 | MetadataReference.CreateFromFile(typeof(IServiceCollection).Assembly.Location), 275 | MetadataReference.CreateFromFile(typeof(External.IExternalService).Assembly.Location), 276 | ], 277 | new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator.Tests/DiagnosticTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Xunit; 5 | 6 | namespace ServiceScan.SourceGenerator.Tests; 7 | 8 | public class DiagnosticTests 9 | { 10 | private const string Services = """ 11 | namespace GeneratorTests; 12 | 13 | public interface IService { } 14 | public class MyService : IService { } 15 | """; 16 | 17 | private readonly DependencyInjectionGenerator _generator = new(); 18 | 19 | private static Compilation CreateCompilation(params string[] source) 20 | { 21 | var path = Path.GetDirectoryName(typeof(object).Assembly.Location)!; 22 | var runtimeAssemblyPath = Path.Combine(path, "System.Runtime.dll"); 23 | 24 | var runtimeReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); 25 | 26 | return CSharpCompilation.Create("compilation", 27 | source.Select(s => CSharpSyntaxTree.ParseText(s)), 28 | [ 29 | MetadataReference.CreateFromFile(typeof(object).Assembly.Location), 30 | MetadataReference.CreateFromFile(runtimeAssemblyPath), 31 | MetadataReference.CreateFromFile(typeof(IServiceCollection).Assembly.Location), 32 | MetadataReference.CreateFromFile(typeof(External.IExternalService).Assembly.Location), 33 | ], 34 | new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); 35 | } 36 | 37 | [Fact] 38 | public void AttributeAddedToNonPartialMethod() 39 | { 40 | var compilation = CreateCompilation(Services, 41 | """ 42 | using ServiceScan.SourceGenerator; 43 | using Microsoft.Extensions.DependencyInjection; 44 | 45 | namespace GeneratorTests; 46 | 47 | public static class ServicesExtensions 48 | { 49 | [GenerateServiceRegistrations(AssignableTo = typeof(IService))] 50 | public static void AddServices(this IServiceCollection services) {} 51 | } 52 | """); 53 | 54 | var results = CSharpGeneratorDriver 55 | .Create(_generator) 56 | .RunGenerators(compilation) 57 | .GetRunResult(); 58 | 59 | Assert.Equal(results.Diagnostics.Single().Descriptor, DiagnosticDescriptors.NotPartialDefinition); 60 | } 61 | 62 | [Fact] 63 | public void AttributeAddedToMethodReturningWrongType() 64 | { 65 | var compilation = CreateCompilation(Services, 66 | """ 67 | using ServiceScan.SourceGenerator; 68 | using Microsoft.Extensions.DependencyInjection; 69 | 70 | namespace GeneratorTests; 71 | 72 | public static partial class ServicesExtensions 73 | { 74 | [GenerateServiceRegistrations(AssignableTo = typeof(IService))] 75 | public static partial IService AddServices(this IServiceCollection services); 76 | } 77 | """); 78 | 79 | var results = CSharpGeneratorDriver 80 | .Create(_generator) 81 | .RunGenerators(compilation) 82 | .GetRunResult(); 83 | 84 | Assert.Equal(results.Diagnostics.Single().Descriptor, DiagnosticDescriptors.WrongReturnType); 85 | } 86 | 87 | [Fact] 88 | public void AttributeAddedToMethodWithoutParameters() 89 | { 90 | var compilation = CreateCompilation(Services, 91 | """ 92 | using ServiceScan.SourceGenerator; 93 | using Microsoft.Extensions.DependencyInjection; 94 | 95 | namespace GeneratorTests; 96 | 97 | public static partial class ServicesExtensions 98 | { 99 | [GenerateServiceRegistrations(AssignableTo = typeof(IService))] 100 | public static partial IServiceCollection AddServices(); 101 | } 102 | """); 103 | 104 | var results = CSharpGeneratorDriver 105 | .Create(_generator) 106 | .RunGenerators(compilation) 107 | .GetRunResult(); 108 | 109 | Assert.Equal(results.Diagnostics.Single().Descriptor, DiagnosticDescriptors.WrongMethodParameters); 110 | } 111 | 112 | [Fact] 113 | public void AttributeAddedToMethodWithWrongParameter() 114 | { 115 | var compilation = CreateCompilation(Services, 116 | """ 117 | using ServiceScan.SourceGenerator; 118 | using Microsoft.Extensions.DependencyInjection; 119 | 120 | namespace GeneratorTests; 121 | 122 | public static partial class ServicesExtensions 123 | { 124 | [GenerateServiceRegistrations(AssignableTo = typeof(IService))] 125 | public static partial IServiceCollection AddServices(IService service); 126 | } 127 | """); 128 | 129 | var results = CSharpGeneratorDriver 130 | .Create(_generator) 131 | .RunGenerators(compilation) 132 | .GetRunResult(); 133 | 134 | Assert.Equal(results.Diagnostics.Single().Descriptor, DiagnosticDescriptors.WrongMethodParameters); 135 | } 136 | 137 | [Fact] 138 | public void SearchCriteriaInTheAttributeProducesNoResults_ReturnsIServiceCollection() 139 | { 140 | var compilation = CreateCompilation(Services, 141 | """ 142 | using ServiceScan.SourceGenerator; 143 | using Microsoft.Extensions.DependencyInjection; 144 | 145 | namespace GeneratorTests; 146 | 147 | public interface IHasNoImplementations { } 148 | 149 | public static partial class ServicesExtensions 150 | { 151 | [GenerateServiceRegistrations(AssignableTo = typeof(IHasNoImplementations))] 152 | public static partial IServiceCollection AddServices(this IServiceCollection services); 153 | } 154 | """); 155 | 156 | var results = CSharpGeneratorDriver 157 | .Create(_generator) 158 | .RunGenerators(compilation) 159 | .GetRunResult(); 160 | 161 | Assert.Equal(results.Diagnostics.Single().Descriptor, DiagnosticDescriptors.NoMatchingTypesFound); 162 | 163 | var expectedFile = """ 164 | namespace GeneratorTests; 165 | 166 | public static partial class ServicesExtensions 167 | { 168 | public static partial global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddServices(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) 169 | { 170 | return services; 171 | } 172 | } 173 | """; 174 | Assert.Equal(expectedFile, results.GeneratedTrees[1].ToString()); 175 | } 176 | 177 | [Fact] 178 | public void SearchCriteriaInTheAttributeProducesNoResults_ReturnsVoid() 179 | { 180 | var compilation = CreateCompilation(Services, 181 | """ 182 | using ServiceScan.SourceGenerator; 183 | using Microsoft.Extensions.DependencyInjection; 184 | 185 | namespace GeneratorTests; 186 | 187 | public interface IHasNoImplementations { } 188 | 189 | public static partial class ServicesExtensions 190 | { 191 | [GenerateServiceRegistrations(AssignableTo = typeof(IHasNoImplementations))] 192 | public static partial void AddServices(this IServiceCollection services); 193 | } 194 | """); 195 | 196 | var results = CSharpGeneratorDriver 197 | .Create(_generator) 198 | .RunGenerators(compilation) 199 | .GetRunResult(); 200 | 201 | Assert.Equal(results.Diagnostics.Single().Descriptor, DiagnosticDescriptors.NoMatchingTypesFound); 202 | 203 | var expectedFile = """ 204 | namespace GeneratorTests; 205 | 206 | public static partial class ServicesExtensions 207 | { 208 | public static partial void AddServices(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) 209 | { 210 | 211 | } 212 | } 213 | """; 214 | Assert.Equal(expectedFile, results.GeneratedTrees[1].ToString()); 215 | } 216 | 217 | [Fact] 218 | public void SearchCriteriaInTheAttributeIsMissing() 219 | { 220 | var compilation = CreateCompilation(Services, 221 | """ 222 | using ServiceScan.SourceGenerator; 223 | using Microsoft.Extensions.DependencyInjection; 224 | 225 | namespace GeneratorTests; 226 | 227 | public static partial class ServicesExtensions 228 | { 229 | [GenerateServiceRegistrations] 230 | public static partial IServiceCollection AddServices(this IServiceCollection services); 231 | } 232 | """); 233 | 234 | var results = CSharpGeneratorDriver 235 | .Create(_generator) 236 | .RunGenerators(compilation) 237 | .GetRunResult(); 238 | 239 | Assert.Equal(results.Diagnostics.Single().Descriptor, DiagnosticDescriptors.MissingSearchCriteria); 240 | } 241 | 242 | [Fact] 243 | public void KeySelectorMethod_GenericButHasParameters() 244 | { 245 | var attribute = @" 246 | private static string GetName(string name) => typeof(T).Name.Replace(""Service"", name); 247 | 248 | [GenerateServiceRegistrations(AssignableTo = typeof(IService), KeySelector = nameof(GetName))]"; 249 | 250 | var compilation = CreateCompilation( 251 | Sources.MethodWithAttribute(attribute), 252 | """ 253 | namespace GeneratorTests; 254 | 255 | public interface IService { } 256 | public class MyService1 : IService { } 257 | public class MyService2 : IService { } 258 | """); 259 | 260 | var results = CSharpGeneratorDriver 261 | .Create(_generator) 262 | .RunGenerators(compilation) 263 | .GetRunResult(); 264 | 265 | Assert.Equal(results.Diagnostics.Single().Descriptor, DiagnosticDescriptors.KeySelectorMethodHasIncorrectSignature); 266 | } 267 | 268 | [Fact] 269 | public void KeySelectorMethod_NonGenericWithoutParameters() 270 | { 271 | var attribute = @" 272 | private static string GetName() => ""const""; 273 | 274 | [GenerateServiceRegistrations(AssignableTo = typeof(IService), KeySelector = nameof(GetName))]"; 275 | 276 | var compilation = CreateCompilation( 277 | Sources.MethodWithAttribute(attribute), 278 | """ 279 | namespace GeneratorTests; 280 | 281 | public interface IService { } 282 | public class MyService1 : IService { } 283 | public class MyService2 : IService { } 284 | """); 285 | 286 | var results = CSharpGeneratorDriver 287 | .Create(_generator) 288 | .RunGenerators(compilation) 289 | .GetRunResult(); 290 | 291 | Assert.Equal(results.Diagnostics.Single().Descriptor, DiagnosticDescriptors.KeySelectorMethodHasIncorrectSignature); 292 | } 293 | 294 | [Fact] 295 | public void KeySelectorMethod_Void() 296 | { 297 | var attribute = @" 298 | private static void GetName(Type type) 299 | { 300 | type.Name.ToString(); 301 | } 302 | 303 | [GenerateServiceRegistrations(AssignableTo = typeof(IService), KeySelector = nameof(GetName))]"; 304 | 305 | var compilation = CreateCompilation( 306 | Sources.MethodWithAttribute(attribute), 307 | """ 308 | namespace GeneratorTests; 309 | 310 | public interface IService { } 311 | public class MyService1 : IService { } 312 | public class MyService2 : IService { } 313 | """); 314 | 315 | var results = CSharpGeneratorDriver 316 | .Create(_generator) 317 | .RunGenerators(compilation) 318 | .GetRunResult(); 319 | 320 | Assert.Equal(results.Diagnostics.Single().Descriptor, DiagnosticDescriptors.KeySelectorMethodHasIncorrectSignature); 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator/DependencyInjectionGenerator.FilterTypes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Linq; 5 | using System.Text.RegularExpressions; 6 | using Microsoft.CodeAnalysis; 7 | using ServiceScan.SourceGenerator.Model; 8 | 9 | namespace ServiceScan.SourceGenerator; 10 | 11 | public partial class DependencyInjectionGenerator 12 | { 13 | private static IEnumerable<(INamedTypeSymbol Type, INamedTypeSymbol[]? MatchedAssignableTypes)> FilterTypes 14 | (Compilation compilation, AttributeModel attribute, INamedTypeSymbol containingType) 15 | { 16 | var semanticModel = compilation.GetSemanticModel(attribute.Location.SourceTree); 17 | var position = attribute.Location.SourceSpan.Start; 18 | 19 | var assemblies = GetAssembliesToScan(compilation, attribute, containingType); 20 | 21 | var assignableToType = attribute.AssignableToTypeName is null 22 | ? null 23 | : compilation.GetTypeByMetadataName(attribute.AssignableToTypeName); 24 | 25 | var excludeAssignableToType = attribute.ExcludeAssignableToTypeName is null 26 | ? null 27 | : compilation.GetTypeByMetadataName(attribute.ExcludeAssignableToTypeName); 28 | 29 | var attributeFilterType = attribute.AttributeFilterTypeName is null 30 | ? null 31 | : compilation.GetTypeByMetadataName(attribute.AttributeFilterTypeName); 32 | 33 | var excludeByAttributeType = attribute.ExcludeByAttributeTypeName is null 34 | ? null 35 | : compilation.GetTypeByMetadataName(attribute.ExcludeByAttributeTypeName); 36 | 37 | var typeNameFilterRegex = BuildWildcardRegex(attribute.TypeNameFilter); 38 | var excludeByTypeNameRegex = BuildWildcardRegex(attribute.ExcludeByTypeName); 39 | 40 | if (assignableToType != null && attribute.AssignableToGenericArguments != null) 41 | { 42 | var typeArguments = attribute.AssignableToGenericArguments.Value.Select(t => compilation.GetTypeByMetadataName(t)).ToArray(); 43 | assignableToType = assignableToType.Construct(typeArguments); 44 | } 45 | 46 | if (excludeAssignableToType != null && attribute.ExcludeAssignableToGenericArguments != null) 47 | { 48 | var typeArguments = attribute.ExcludeAssignableToGenericArguments.Value.Select(t => compilation.GetTypeByMetadataName(t)).ToArray(); 49 | excludeAssignableToType = excludeAssignableToType.Construct(typeArguments); 50 | } 51 | 52 | var customHandlerMethod = attribute.CustomHandler != null && attribute.CustomHandlerType == CustomHandlerType.Method 53 | ? containingType.GetMembers().OfType().FirstOrDefault(m => m.Name == attribute.CustomHandler) 54 | : null; 55 | 56 | foreach (var type in assemblies.SelectMany(GetTypesFromAssembly)) 57 | { 58 | if (type.IsAbstract || !type.CanBeReferencedByName || type.TypeKind != TypeKind.Class) 59 | continue; 60 | 61 | // Static types are allowed for custom handlers (with type method) 62 | if (type.IsStatic && attribute.CustomHandlerType != CustomHandlerType.TypeMethod) 63 | continue; 64 | 65 | // Cannot use open generics with CustomHandler 66 | if (type.IsGenericType && attribute.CustomHandler != null) 67 | continue; 68 | 69 | if (attributeFilterType != null) 70 | { 71 | if (!type.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeFilterType))) 72 | continue; 73 | } 74 | 75 | if (excludeByAttributeType != null) 76 | { 77 | if (type.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, excludeByAttributeType))) 78 | continue; 79 | } 80 | 81 | if (typeNameFilterRegex != null && !typeNameFilterRegex.IsMatch(type.ToDisplayString())) 82 | continue; 83 | 84 | if (excludeByTypeNameRegex != null && excludeByTypeNameRegex.IsMatch(type.ToDisplayString())) 85 | continue; 86 | 87 | if (excludeAssignableToType != null && IsAssignableTo(type, excludeAssignableToType, out _)) 88 | continue; 89 | 90 | INamedTypeSymbol[] matchedTypes = null; 91 | if (assignableToType != null && !IsAssignableTo(type, assignableToType, out matchedTypes)) 92 | continue; 93 | 94 | // Filter by custom handler method generic constraints 95 | if (customHandlerMethod != null && !SatisfiesGenericConstraints(type, customHandlerMethod)) 96 | { 97 | continue; 98 | } 99 | 100 | if (!semanticModel.IsAccessible(position, type)) 101 | continue; 102 | 103 | yield return (type, matchedTypes); 104 | } 105 | } 106 | 107 | private static bool IsAssignableTo(INamedTypeSymbol type, INamedTypeSymbol assignableTo, out INamedTypeSymbol[]? matchedTypes) 108 | { 109 | if (SymbolEqualityComparer.Default.Equals(type, assignableTo)) 110 | { 111 | matchedTypes = [type]; 112 | return true; 113 | } 114 | 115 | if (assignableTo.IsGenericType && assignableTo.IsDefinition) 116 | { 117 | if (assignableTo.TypeKind == TypeKind.Interface) 118 | { 119 | matchedTypes = type.AllInterfaces 120 | .Where(i => i.IsGenericType && SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, assignableTo)) 121 | .ToArray(); 122 | 123 | return matchedTypes.Length > 0; 124 | } 125 | 126 | var baseType = type.BaseType; 127 | while (baseType != null) 128 | { 129 | if (baseType.IsGenericType && SymbolEqualityComparer.Default.Equals(baseType.OriginalDefinition, assignableTo)) 130 | { 131 | matchedTypes = [baseType]; 132 | return true; 133 | } 134 | 135 | baseType = baseType.BaseType; 136 | } 137 | } 138 | else 139 | { 140 | if (assignableTo.TypeKind == TypeKind.Interface) 141 | { 142 | matchedTypes = [assignableTo]; 143 | return type.AllInterfaces.Contains(assignableTo, SymbolEqualityComparer.Default); 144 | } 145 | 146 | var baseType = type.BaseType; 147 | while (baseType != null) 148 | { 149 | if (SymbolEqualityComparer.Default.Equals(baseType, assignableTo)) 150 | { 151 | matchedTypes = [baseType]; 152 | return true; 153 | } 154 | 155 | baseType = baseType.BaseType; 156 | } 157 | } 158 | 159 | matchedTypes = null; 160 | return false; 161 | } 162 | 163 | private static IEnumerable GetAssembliesToScan(Compilation compilation, AttributeModel attribute, INamedTypeSymbol containingType) 164 | { 165 | var assemblyOfType = attribute.AssemblyOfTypeName is null 166 | ? null 167 | : compilation.GetTypeByMetadataName(attribute.AssemblyOfTypeName); 168 | 169 | if (assemblyOfType is not null) 170 | { 171 | return [assemblyOfType.ContainingAssembly]; 172 | } 173 | 174 | if (attribute.AssemblyNameFilter is not null) 175 | { 176 | var assemblyNameRegex = BuildWildcardRegex(attribute.AssemblyNameFilter); 177 | 178 | return new[] { compilation.Assembly } 179 | .Concat(compilation.SourceModule.ReferencedAssemblySymbols) 180 | .Where(assembly => assemblyNameRegex.IsMatch(assembly.Name)); 181 | } 182 | 183 | return [containingType.ContainingAssembly]; 184 | } 185 | 186 | private static IEnumerable GetTypesFromAssembly(IAssemblySymbol assemblySymbol) 187 | { 188 | var @namespace = assemblySymbol.GlobalNamespace; 189 | return GetTypesFromNamespaceOrType(@namespace); 190 | 191 | static IEnumerable GetTypesFromNamespaceOrType(INamespaceOrTypeSymbol symbol) 192 | { 193 | foreach (var member in symbol.GetMembers()) 194 | { 195 | if (member is INamespaceOrTypeSymbol namespaceOrType) 196 | { 197 | if (member is INamedTypeSymbol namedType) 198 | { 199 | yield return namedType; 200 | } 201 | 202 | foreach (var type in GetTypesFromNamespaceOrType(namespaceOrType)) 203 | { 204 | yield return type; 205 | } 206 | } 207 | } 208 | } 209 | } 210 | 211 | [return: NotNullIfNotNull(nameof(wildcard))] 212 | private static Regex? BuildWildcardRegex(string? wildcard) 213 | { 214 | return wildcard is null 215 | ? null 216 | : new Regex($"^({Regex.Escape(wildcard).Replace(@"\*", ".*").Replace(",", "|")})$"); 217 | } 218 | 219 | private static bool SatisfiesGenericConstraints(INamedTypeSymbol type, IMethodSymbol customHandlerMethod) 220 | { 221 | if (customHandlerMethod.TypeParameters.Length == 0) 222 | return true; 223 | 224 | // Check constraints on the first type parameter (which will be the implementation type) 225 | // (Other type parameters could be checked recursively from the first type parameter) 226 | var typeParameter = customHandlerMethod.TypeParameters[0]; 227 | 228 | var visitedTypeParameters = new HashSet(SymbolEqualityComparer.Default); 229 | return SatisfiesGenericConstraints(type, typeParameter, customHandlerMethod, visitedTypeParameters); 230 | } 231 | 232 | private static bool SatisfiesGenericConstraints(INamedTypeSymbol type, ITypeParameterSymbol typeParameter, IMethodSymbol customHandlerMethod, HashSet visitedTypeParameters) 233 | { 234 | // Prevent infinite recursion in circular constraint scenarios (e.g., X : ISmth, Y : ISmth) 235 | if (!visitedTypeParameters.Add(typeParameter)) 236 | return true; 237 | 238 | // Check reference type constraint 239 | if (typeParameter.HasReferenceTypeConstraint && type.IsValueType) 240 | return false; 241 | 242 | // Check value type constraint 243 | if (typeParameter.HasValueTypeConstraint && !type.IsValueType) 244 | return false; 245 | 246 | // Check unmanaged type constraint 247 | if (typeParameter.HasUnmanagedTypeConstraint && !type.IsUnmanagedType) 248 | return false; 249 | 250 | // Check constructor constraint 251 | if (typeParameter.HasConstructorConstraint) 252 | { 253 | var hasPublicParameterlessConstructor = type.Constructors.Any(c => 254 | c.DeclaredAccessibility == Accessibility.Public && 255 | c.Parameters.Length == 0 && 256 | !c.IsStatic); 257 | 258 | if (!hasPublicParameterlessConstructor) 259 | return false; 260 | } 261 | 262 | // Check type constraints 263 | foreach (var constraintType in typeParameter.ConstraintTypes) 264 | { 265 | if (constraintType is INamedTypeSymbol namedConstraintType) 266 | { 267 | if (!SatisfiesConstraintType(type, namedConstraintType, customHandlerMethod, visitedTypeParameters)) 268 | return false; 269 | } 270 | } 271 | 272 | return true; 273 | } 274 | 275 | private static bool SatisfiesConstraintType(INamedTypeSymbol candidateType, INamedTypeSymbol constraintType, IMethodSymbol customHandlerMethod, HashSet visitedTypeParameters) 276 | { 277 | var constraintHasTypeParameters = constraintType.TypeArguments.OfType().Any(); 278 | 279 | if (!constraintHasTypeParameters) 280 | { 281 | return IsAssignableTo(candidateType, constraintType, out _); 282 | } 283 | else 284 | { 285 | // We handle the case when method has multiple type arguments, e.g. 286 | // private static void CustomHandler(this IServiceCollection services) 287 | // where THandler : class, ICommandHandler 288 | // where TCommand : ISpecificCommand 289 | 290 | 291 | // First we check that type definitions match. E.g. if MyHandlerImplementation has interface (one or many) ICommandHandler<>. 292 | if (!IsAssignableTo(candidateType, constraintType.OriginalDefinition, out var matchedTypes)) 293 | return false; 294 | 295 | // Then we need to check if any matched interfaces (let's say MyHandlerImplementation implements ICommandHandler and ICommandHandler) 296 | // have matching type parameters (e.g. string does not implement ISpecificCommand, but MySpecificCommand - does). 297 | return matchedTypes.Any(matchedType => MatchedTypeSatisfiesConstraints(constraintType, customHandlerMethod, matchedType, visitedTypeParameters)); 298 | } 299 | 300 | static bool MatchedTypeSatisfiesConstraints(INamedTypeSymbol constraintType, IMethodSymbol customHandlerMethod, INamedTypeSymbol matchedType, HashSet visitedTypeParameters) 301 | { 302 | if (constraintType.TypeArguments.Length != matchedType.TypeArguments.Length) 303 | return false; 304 | 305 | for (var i = 0; i < constraintType.TypeArguments.Length; i++) 306 | { 307 | if (matchedType.TypeArguments[i] is not INamedTypeSymbol candidateTypeArgument) 308 | return false; 309 | 310 | if (constraintType.TypeArguments[i] is ITypeParameterSymbol typeParameter) 311 | { 312 | if (!SatisfiesGenericConstraints(candidateTypeArgument, typeParameter, customHandlerMethod, visitedTypeParameters)) 313 | return false; 314 | } 315 | else 316 | { 317 | if (!SymbolEqualityComparer.Default.Equals(candidateTypeArgument, constraintType.TypeArguments[i])) 318 | return false; 319 | } 320 | } 321 | 322 | return true; 323 | } 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator.Tests/CustomHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Xunit; 5 | 6 | namespace ServiceScan.SourceGenerator.Tests; 7 | 8 | public class CustomHandlerTests 9 | { 10 | private readonly DependencyInjectionGenerator _generator = new(); 11 | 12 | [Fact] 13 | public void CustomHandlerWithNoParameters() 14 | { 15 | var source = $$""" 16 | using ServiceScan.SourceGenerator; 17 | 18 | namespace GeneratorTests; 19 | 20 | public static partial class ServicesExtensions 21 | { 22 | [GenerateServiceRegistrations(AssignableTo = typeof(IService), CustomHandler = nameof(HandleType))] 23 | public static partial void ProcessServices(); 24 | 25 | private static void HandleType() => System.Console.WriteLine(typeof(T).Name); 26 | } 27 | """; 28 | 29 | var services = 30 | """ 31 | namespace GeneratorTests; 32 | 33 | public interface IService { } 34 | public class MyService1 : IService { } 35 | public class MyService2 : IService { } 36 | """; 37 | 38 | var compilation = CreateCompilation(source, services); 39 | 40 | var results = CSharpGeneratorDriver 41 | .Create(_generator) 42 | .RunGenerators(compilation) 43 | .GetRunResult(); 44 | 45 | var expected = $$""" 46 | namespace GeneratorTests; 47 | 48 | public static partial class ServicesExtensions 49 | { 50 | public static partial void ProcessServices() 51 | { 52 | HandleType(); 53 | HandleType(); 54 | } 55 | } 56 | """; 57 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 58 | } 59 | 60 | [Fact] 61 | public void CustomHandlerWithParameters() 62 | { 63 | var source = $$""" 64 | using ServiceScan.SourceGenerator; 65 | 66 | namespace GeneratorTests; 67 | 68 | public static partial class ServicesExtensions 69 | { 70 | [GenerateServiceRegistrations(TypeNameFilter = "*Service", CustomHandler = nameof(HandleType))] 71 | public static partial void ProcessServices(string value, decimal number); 72 | 73 | private static void HandleType(string value, decimal number) => System.Console.WriteLine(value + number.ToString() + typeof(T).Name); 74 | } 75 | """; 76 | 77 | var services = 78 | """ 79 | namespace GeneratorTests; 80 | 81 | public class MyFirstService {} 82 | public class MySecondService {} 83 | public class ServiceWithNonMatchingName {} 84 | """; 85 | 86 | var compilation = CreateCompilation(source, services); 87 | 88 | var results = CSharpGeneratorDriver 89 | .Create(_generator) 90 | .RunGenerators(compilation) 91 | .GetRunResult(); 92 | 93 | var expected = $$""" 94 | namespace GeneratorTests; 95 | 96 | public static partial class ServicesExtensions 97 | { 98 | public static partial void ProcessServices( string value, decimal number) 99 | { 100 | HandleType(value, number); 101 | HandleType(value, number); 102 | } 103 | } 104 | """; 105 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 106 | } 107 | 108 | [Fact] 109 | public void CustomHandler_NoTypesFound() 110 | { 111 | var source = $$""" 112 | using ServiceScan.SourceGenerator; 113 | 114 | namespace GeneratorTests; 115 | 116 | public static partial class ServicesExtensions 117 | { 118 | [GenerateServiceRegistrations(AssignableTo = typeof(IService), CustomHandler = nameof(HandleType))] 119 | public static partial void ProcessServices(); 120 | 121 | private static void HandleType() => System.Console.WriteLine(typeof(T).Name); 122 | } 123 | """; 124 | 125 | var services = 126 | """ 127 | namespace GeneratorTests; 128 | 129 | public interface IService { } 130 | """; 131 | 132 | var compilation = CreateCompilation(source, services); 133 | 134 | var results = CSharpGeneratorDriver 135 | .Create(_generator) 136 | .RunGenerators(compilation) 137 | .GetRunResult(); 138 | 139 | var expected = $$""" 140 | namespace GeneratorTests; 141 | 142 | public static partial class ServicesExtensions 143 | { 144 | public static partial void ProcessServices() 145 | { 146 | 147 | } 148 | } 149 | """; 150 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 151 | } 152 | 153 | [Fact] 154 | public void CustomHandlerExtensionMethod() 155 | { 156 | var source = $$""" 157 | using ServiceScan.SourceGenerator; 158 | 159 | namespace GeneratorTests; 160 | 161 | public static partial class ServicesExtensions 162 | { 163 | [GenerateServiceRegistrations(AssignableTo = typeof(IService), CustomHandler = nameof(HandleType))] 164 | public static partial IServices ProcessServices(this IServices services); 165 | 166 | private static void HandleType(IServices services) where T:IService, new() => services.Add(new T()); 167 | } 168 | """; 169 | 170 | var services = 171 | """ 172 | namespace GeneratorTests; 173 | 174 | public interface IServices 175 | { 176 | void Add(IService service); 177 | } 178 | 179 | public interface IService { } 180 | public class MyService1 : IService { } 181 | public class MyService2 : IService { } 182 | """; 183 | 184 | var compilation = CreateCompilation(source, services); 185 | 186 | var results = CSharpGeneratorDriver 187 | .Create(_generator) 188 | .RunGenerators(compilation) 189 | .GetRunResult(); 190 | 191 | var expected = $$""" 192 | namespace GeneratorTests; 193 | 194 | public static partial class ServicesExtensions 195 | { 196 | public static partial global::GeneratorTests.IServices ProcessServices(this global::GeneratorTests.IServices services) 197 | { 198 | HandleType(services); 199 | HandleType(services); 200 | return services; 201 | } 202 | } 203 | """; 204 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 205 | } 206 | 207 | [Fact] 208 | public void CustomHandlerWithParametersAndAttributeFilter() 209 | { 210 | var source = $$""" 211 | using ServiceScan.SourceGenerator; 212 | 213 | namespace GeneratorTests; 214 | 215 | public static partial class ServicesExtensions 216 | { 217 | [GenerateServiceRegistrations(AttributeFilter = typeof(ServiceAttribute), CustomHandler = nameof(HandleType))] 218 | public static partial IServiceCollection ProcessServices(this IServiceCollection services, decimal number); 219 | 220 | private static void HandleType(IServiceCollection services, decimal number) => System.Console.WriteLine(number.ToString() + typeof(T).Name); 221 | } 222 | """; 223 | 224 | var services = 225 | """ 226 | using System; 227 | 228 | namespace GeneratorTests; 229 | 230 | [AttributeUsage(AttributeTargets.Class)] 231 | public sealed class ServiceAttribute : Attribute; 232 | 233 | [Service] 234 | public class MyFirstService {} 235 | 236 | [Service] 237 | public class MySecondService {} 238 | 239 | public class ServiceWithoutAttribute {} 240 | """; 241 | 242 | var compilation = CreateCompilation(source, services); 243 | 244 | var results = CSharpGeneratorDriver 245 | .Create(_generator) 246 | .RunGenerators(compilation) 247 | .GetRunResult(); 248 | 249 | var expected = $$""" 250 | namespace GeneratorTests; 251 | 252 | public static partial class ServicesExtensions 253 | { 254 | public static partial IServiceCollection ProcessServices(this IServiceCollection services, decimal number) 255 | { 256 | HandleType(services, number); 257 | HandleType(services, number); 258 | return services; 259 | } 260 | } 261 | """; 262 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 263 | } 264 | 265 | [Fact] 266 | public void AddMultipleCustomHandlerAttributesWithDifferentCustomHandler() 267 | { 268 | var source = $$""" 269 | using ServiceScan.SourceGenerator; 270 | 271 | namespace GeneratorTests; 272 | 273 | public static partial class ServicesExtensions 274 | { 275 | [GenerateServiceRegistrations(AssignableTo = typeof(IFirstService), CustomHandler = nameof(HandleFirstType))] 276 | [GenerateServiceRegistrations(AssignableTo = typeof(ISecondService), CustomHandler = nameof(HandleSecondType))] 277 | public static partial void ProcessServices(); 278 | 279 | private static void HandleFirstType() => System.Console.WriteLine("First:" + typeof(T).Name); 280 | private static void HandleSecondType() => System.Console.WriteLine("Second:" + typeof(T).Name); 281 | } 282 | """; 283 | 284 | var services = 285 | """ 286 | namespace GeneratorTests; 287 | 288 | public interface IFirstService { } 289 | public interface ISecondService { } 290 | public class MyService1 : IFirstService { } 291 | public class MyService2 : ISecondService { } 292 | """; 293 | 294 | var compilation = CreateCompilation(source, services); 295 | 296 | var results = CSharpGeneratorDriver 297 | .Create(_generator) 298 | .RunGenerators(compilation) 299 | .GetRunResult(); 300 | 301 | var expected = $$""" 302 | namespace GeneratorTests; 303 | 304 | public static partial class ServicesExtensions 305 | { 306 | public static partial void ProcessServices() 307 | { 308 | HandleFirstType(); 309 | HandleSecondType(); 310 | } 311 | } 312 | """; 313 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 314 | } 315 | 316 | [Fact] 317 | public void AddMultipleCustomHandlerAttributesWithSameCustomHandler() 318 | { 319 | var source = $$""" 320 | using ServiceScan.SourceGenerator; 321 | 322 | namespace GeneratorTests; 323 | 324 | public static partial class ServicesExtensions 325 | { 326 | [GenerateServiceRegistrations(AssignableTo = typeof(IFirstService), CustomHandler = nameof(HandleType))] 327 | [GenerateServiceRegistrations(AssignableTo = typeof(ISecondService), CustomHandler = nameof(HandleType))] 328 | public static partial void ProcessServices(); 329 | 330 | private static void HandleType() => System.Console.WriteLine(typeof(T).Name); 331 | } 332 | """; 333 | 334 | var services = 335 | """ 336 | namespace GeneratorTests; 337 | 338 | public interface IFirstService { } 339 | public interface ISecondService { } 340 | public class MyService1 : IFirstService { } 341 | public class MyService2 : ISecondService { } 342 | """; 343 | 344 | var compilation = CreateCompilation(source, services); 345 | 346 | var results = CSharpGeneratorDriver 347 | .Create(_generator) 348 | .RunGenerators(compilation) 349 | .GetRunResult(); 350 | 351 | var expected = $$""" 352 | namespace GeneratorTests; 353 | 354 | public static partial class ServicesExtensions 355 | { 356 | public static partial void ProcessServices() 357 | { 358 | HandleType(); 359 | HandleType(); 360 | } 361 | } 362 | """; 363 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 364 | } 365 | 366 | [Fact] 367 | public void ResolveCustomHandlerGenericArguments() 368 | { 369 | var source = $$""" 370 | using ServiceScan.SourceGenerator; 371 | 372 | namespace GeneratorTests; 373 | 374 | public static partial class ModelBuilderExtensions 375 | { 376 | [GenerateServiceRegistrations(AssignableTo = typeof(IEntityTypeConfiguration<>), CustomHandler = nameof(ApplyConfiguration))] 377 | public static partial ModelBuilder ApplyEntityConfigurations(this ModelBuilder modelBuilder); 378 | 379 | private static void ApplyConfiguration(ModelBuilder modelBuilder) 380 | where T : IEntityTypeConfiguration, new() 381 | where TEntity : class 382 | { 383 | modelBuilder.ApplyConfiguration(new T()); 384 | } 385 | } 386 | """; 387 | 388 | var infra = """ 389 | public interface IEntityTypeConfiguration where TEntity : class 390 | { 391 | void Configure(EntityTypeBuilder builder); 392 | } 393 | 394 | public class EntityTypeBuilder where TEntity : class; 395 | 396 | public class ModelBuilder 397 | { 398 | public ModelBuilder ApplyConfiguration(IEntityTypeConfiguration configuration) where TEntity : class 399 | { 400 | return this; 401 | } 402 | } 403 | """; 404 | 405 | var configurations = """ 406 | namespace GeneratorTests; 407 | 408 | public class EntityA; 409 | public class EntityB; 410 | 411 | public class EntityAConfiguration : IEntityTypeConfiguration 412 | { 413 | public void Configure(EntityTypeBuilder builder) { } 414 | } 415 | 416 | public class EntityBConfiguration : IEntityTypeConfiguration 417 | { 418 | public void Configure(EntityTypeBuilder builder) { } 419 | } 420 | """; 421 | 422 | var compilation = CreateCompilation(source, infra, configurations); 423 | 424 | var results = CSharpGeneratorDriver 425 | .Create(_generator) 426 | .RunGenerators(compilation) 427 | .GetRunResult(); 428 | 429 | var expected = $$""" 430 | namespace GeneratorTests; 431 | 432 | public static partial class ModelBuilderExtensions 433 | { 434 | public static partial global::ModelBuilder ApplyEntityConfigurations(this global::ModelBuilder modelBuilder) 435 | { 436 | ApplyConfiguration(modelBuilder); 437 | ApplyConfiguration(modelBuilder); 438 | return modelBuilder; 439 | } 440 | } 441 | """; 442 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 443 | } 444 | 445 | [Fact] 446 | public void UseInstanceCustomHandlerMethod() 447 | { 448 | var source = $$""" 449 | using ServiceScan.SourceGenerator; 450 | 451 | namespace GeneratorTests; 452 | 453 | public partial class ServicesExtensions 454 | { 455 | [GenerateServiceRegistrations(AssignableTo = typeof(IService), CustomHandler = nameof(HandleType))] 456 | public partial void ProcessServices(); 457 | 458 | private void HandleType() => System.Console.WriteLine(typeof(T).Name); 459 | } 460 | """; 461 | 462 | var services = 463 | """ 464 | namespace GeneratorTests; 465 | 466 | public interface IService { } 467 | public class MyService1 : IService { } 468 | public class MyService2 : IService { } 469 | """; 470 | 471 | var compilation = CreateCompilation(source, services); 472 | 473 | var results = CSharpGeneratorDriver 474 | .Create(_generator) 475 | .RunGenerators(compilation) 476 | .GetRunResult(); 477 | 478 | var expected = $$""" 479 | namespace GeneratorTests; 480 | 481 | public partial class ServicesExtensions 482 | { 483 | public partial void ProcessServices() 484 | { 485 | HandleType(); 486 | HandleType(); 487 | } 488 | } 489 | """; 490 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 491 | } 492 | 493 | [Fact] 494 | public void UseInstanceCustomHandlerMethod_FromParentType() 495 | { 496 | var source = $$""" 497 | using ServiceScan.SourceGenerator; 498 | 499 | namespace GeneratorTests; 500 | 501 | public abstract class AbstractServiceProcessor 502 | { 503 | protected void HandleType() => System.Console.WriteLine(typeof(T).Name); 504 | } 505 | 506 | public partial class ServicesProcessor : AbstractServiceProcessor 507 | { 508 | [GenerateServiceRegistrations(AssignableTo = typeof(IService), CustomHandler = nameof(HandleType))] 509 | public partial void ProcessServices(); 510 | } 511 | """; 512 | 513 | var services = 514 | """ 515 | namespace GeneratorTests; 516 | 517 | public interface IService { } 518 | public class MyService1 : IService { } 519 | public class MyService2 : IService { } 520 | """; 521 | 522 | var compilation = CreateCompilation(source, services); 523 | 524 | var results = CSharpGeneratorDriver 525 | .Create(_generator) 526 | .RunGenerators(compilation) 527 | .GetRunResult(); 528 | 529 | var expected = $$""" 530 | namespace GeneratorTests; 531 | 532 | public partial class ServicesProcessor 533 | { 534 | public partial void ProcessServices() 535 | { 536 | HandleType(); 537 | HandleType(); 538 | } 539 | } 540 | """; 541 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 542 | } 543 | 544 | [Fact] 545 | public void UseStaticMethodFromMatchedClassAsCustomHandler_WithoutParameters() 546 | { 547 | var source = $$""" 548 | using ServiceScan.SourceGenerator; 549 | 550 | namespace GeneratorTests; 551 | 552 | public partial class ServicesExtensions 553 | { 554 | [GenerateServiceRegistrations(AssignableTo = typeof(IService), CustomHandler = "Handler"))] 555 | public partial void ProcessServices(); 556 | } 557 | """; 558 | 559 | var services = 560 | """ 561 | namespace GeneratorTests; 562 | 563 | public interface IService { } 564 | 565 | public class MyService1 : IService 566 | { 567 | public static void Handler() { } 568 | } 569 | 570 | public class MyService2 : IService 571 | { 572 | public static void Handler() { } 573 | } 574 | """; 575 | 576 | var compilation = CreateCompilation(source, services); 577 | 578 | var results = CSharpGeneratorDriver 579 | .Create(_generator) 580 | .RunGenerators(compilation) 581 | .GetRunResult(); 582 | 583 | var expected = $$""" 584 | namespace GeneratorTests; 585 | 586 | public partial class ServicesExtensions 587 | { 588 | public partial void ProcessServices() 589 | { 590 | global::GeneratorTests.MyService1.Handler(); 591 | global::GeneratorTests.MyService2.Handler(); 592 | } 593 | } 594 | """; 595 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 596 | } 597 | 598 | [Fact] 599 | public void UseStaticMethodFromMatchedStaticClassAsCustomHandler_WithParameters() 600 | { 601 | var source = $$""" 602 | using ServiceScan.SourceGenerator; 603 | using Microsoft.Extensions.DependencyInjection; 604 | 605 | namespace GeneratorTests; 606 | 607 | public partial class ServicesExtensions 608 | { 609 | [GenerateServiceRegistrations(TypeNameFilter = "*StaticService", CustomHandler = "Handler"))] 610 | public partial void ProcessServices(IServiceCollection services); 611 | } 612 | """; 613 | 614 | var services = 615 | """ 616 | namespace GeneratorTests; 617 | 618 | public static class FirstStaticService 619 | { 620 | public static void Handler(IServiceCollection services) { } 621 | } 622 | 623 | public static class SecondStaticService 624 | { 625 | public static void Handler(IServiceCollection services) { } 626 | } 627 | """; 628 | 629 | var compilation = CreateCompilation(source, services); 630 | 631 | var results = CSharpGeneratorDriver 632 | .Create(_generator) 633 | .RunGenerators(compilation) 634 | .GetRunResult(); 635 | 636 | var expected = $$""" 637 | namespace GeneratorTests; 638 | 639 | public partial class ServicesExtensions 640 | { 641 | public partial void ProcessServices( global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) 642 | { 643 | global::GeneratorTests.FirstStaticService.Handler(services); 644 | global::GeneratorTests.SecondStaticService.Handler(services); 645 | } 646 | } 647 | """; 648 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 649 | } 650 | 651 | [Fact] 652 | public void AddServicesWithDecorator() 653 | { 654 | var services = """ 655 | namespace GeneratorTests; 656 | 657 | public interface ICommandHandler { } 658 | public class CommandHandlerDecorator(ICommandHandler inner) : ICommandHandler; 659 | 660 | public class SpecificHandler1 : ICommandHandler; 661 | public class SpecificHandler2 : ICommandHandler; 662 | """; 663 | 664 | var source = """ 665 | using ServiceScan.SourceGenerator; 666 | using Microsoft.Extensions.DependencyInjection; 667 | 668 | namespace GeneratorTests; 669 | 670 | public static partial class ServiceCollectionExtensions 671 | { 672 | [GenerateServiceRegistrations(AssignableTo = typeof(ICommandHandler<>), CustomHandler = nameof(AddDecoratedHandler))] 673 | public static partial IServiceCollection AddHandlers(this IServiceCollection services); 674 | 675 | private static void AddDecoratedHandler(this IServiceCollection services) 676 | where THandler : class, ICommandHandler 677 | { 678 | // Add handler itself to DI 679 | services.AddScoped(); 680 | 681 | // Register decorated handler as ICommandHandler 682 | services.AddScoped>(s => new CommandHandlerDecorator(s.GetRequiredService())); 683 | } 684 | } 685 | """; 686 | 687 | 688 | var compilation = CreateCompilation(source, services); 689 | 690 | var results = CSharpGeneratorDriver 691 | .Create(_generator) 692 | .RunGenerators(compilation) 693 | .GetRunResult(); 694 | 695 | var expected = $$""" 696 | namespace GeneratorTests; 697 | 698 | public static partial class ServiceCollectionExtensions 699 | { 700 | public static partial global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddHandlers(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services) 701 | { 702 | AddDecoratedHandler(services); 703 | AddDecoratedHandler(services); 704 | return services; 705 | } 706 | } 707 | """; 708 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 709 | } 710 | 711 | [Fact] 712 | public void CustomHandler_FiltersByNewConstraint() 713 | { 714 | var source = """ 715 | using ServiceScan.SourceGenerator; 716 | 717 | namespace GeneratorTests; 718 | 719 | public static partial class ServicesExtensions 720 | { 721 | [GenerateServiceRegistrations(AssignableTo = typeof(IService), CustomHandler = nameof(HandleType))] 722 | public static partial void ProcessServices(); 723 | 724 | private static void HandleType() where T : IService, new() => System.Console.WriteLine(typeof(T).Name); 725 | } 726 | """; 727 | 728 | var services = """ 729 | namespace GeneratorTests; 730 | 731 | public interface IService { } 732 | public class ServiceWithParameterlessConstructor : IService { } 733 | public class ServiceWithoutParameterlessConstructor : IService 734 | { 735 | public ServiceWithoutParameterlessConstructor(int value) { } 736 | } 737 | public class ServiceWithPrivateConstructor : IService 738 | { 739 | private ServiceWithPrivateConstructor() { } 740 | } 741 | """; 742 | 743 | var compilation = CreateCompilation(source, services); 744 | 745 | var results = CSharpGeneratorDriver 746 | .Create(_generator) 747 | .RunGenerators(compilation) 748 | .GetRunResult(); 749 | 750 | var expected = """ 751 | namespace GeneratorTests; 752 | 753 | public static partial class ServicesExtensions 754 | { 755 | public static partial void ProcessServices() 756 | { 757 | HandleType(); 758 | } 759 | } 760 | """; 761 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 762 | } 763 | 764 | [Fact] 765 | public void CustomHandler_FiltersByClassConstraint() 766 | { 767 | var source = """ 768 | using ServiceScan.SourceGenerator; 769 | 770 | namespace GeneratorTests; 771 | 772 | public static partial class ServicesExtensions 773 | { 774 | [GenerateServiceRegistrations(TypeNameFilter = "*Service", CustomHandler = nameof(HandleType))] 775 | public static partial void ProcessServices(); 776 | 777 | private static void HandleType() where T : class => System.Console.WriteLine(typeof(T).Name); 778 | } 779 | """; 780 | 781 | var services = """ 782 | namespace GeneratorTests; 783 | 784 | public class ClassService { } 785 | public struct StructService { } 786 | """; 787 | 788 | var compilation = CreateCompilation(source, services); 789 | 790 | var results = CSharpGeneratorDriver 791 | .Create(_generator) 792 | .RunGenerators(compilation) 793 | .GetRunResult(); 794 | 795 | var expected = """ 796 | namespace GeneratorTests; 797 | 798 | public static partial class ServicesExtensions 799 | { 800 | public static partial void ProcessServices() 801 | { 802 | HandleType(); 803 | } 804 | } 805 | """; 806 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 807 | } 808 | 809 | [Fact] 810 | public void CustomHandler_FiltersByNestedTypeParameterConstraints() 811 | { 812 | var source = """ 813 | using ServiceScan.SourceGenerator; 814 | 815 | namespace GeneratorTests; 816 | 817 | public static partial class ServiceCollectionExtensions 818 | { 819 | [GenerateServiceRegistrations(AssignableTo = typeof(ICommandHandler<>), CustomHandler = nameof(AddHandler))] 820 | public static partial void AddHandlers(); 821 | 822 | private static void AddHandler() 823 | where THandler : class, ICommandHandler 824 | where TCommand : class, ICommand 825 | { 826 | } 827 | } 828 | """; 829 | 830 | var services = """ 831 | namespace GeneratorTests; 832 | 833 | public interface ICommand { } 834 | public interface ICommandHandler where T : ICommand { } 835 | 836 | public class ValidCommand : ICommand { } 837 | public class InvalidCommand { } 838 | 839 | public class ValidHandler : ICommandHandler { } 840 | public class InvalidHandler : ICommandHandler { } 841 | """; 842 | 843 | var compilation = CreateCompilation(source, services); 844 | 845 | var results = CSharpGeneratorDriver 846 | .Create(_generator) 847 | .RunGenerators(compilation) 848 | .GetRunResult(); 849 | 850 | var expected = """ 851 | namespace GeneratorTests; 852 | 853 | public static partial class ServiceCollectionExtensions 854 | { 855 | public static partial void AddHandlers() 856 | { 857 | AddHandler(); 858 | } 859 | } 860 | """; 861 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 862 | } 863 | 864 | [Fact] 865 | public void CustomHandler_FiltersByMultipleInterfacesWithDifferentTypeArguments() 866 | { 867 | var source = """ 868 | using ServiceScan.SourceGenerator; 869 | 870 | namespace GeneratorTests; 871 | 872 | public static partial class ServiceCollectionExtensions 873 | { 874 | [GenerateServiceRegistrations(AssignableTo = typeof(IHandler<>), CustomHandler = nameof(AddHandler))] 875 | public static partial void AddHandlers(); 876 | 877 | private static void AddHandler() 878 | where THandler : class, IHandler 879 | where TArg : class 880 | { 881 | } 882 | } 883 | """; 884 | 885 | var services = """ 886 | namespace GeneratorTests; 887 | 888 | public interface IHandler { } 889 | 890 | public class Handler1 : IHandler { } 891 | public class Handler2 : IHandler { } 892 | public class Handler3 : IHandler { } 893 | public class MultiHandler : IHandler, IHandler { } 894 | """; 895 | 896 | var compilation = CreateCompilation(source, services); 897 | 898 | var results = CSharpGeneratorDriver 899 | .Create(_generator) 900 | .RunGenerators(compilation) 901 | .GetRunResult(); 902 | 903 | var expected = """ 904 | namespace GeneratorTests; 905 | 906 | public static partial class ServiceCollectionExtensions 907 | { 908 | public static partial void AddHandlers() 909 | { 910 | AddHandler(); 911 | AddHandler(); 912 | AddHandler(); 913 | AddHandler(); 914 | } 915 | } 916 | """; 917 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 918 | } 919 | 920 | [Fact] 921 | public void CustomHandler_FiltersByValueTypeConstraint() 922 | { 923 | var source = """ 924 | using ServiceScan.SourceGenerator; 925 | 926 | namespace GeneratorTests; 927 | 928 | public static partial class ServiceCollectionExtensions 929 | { 930 | [GenerateServiceRegistrations(AssignableTo = typeof(IProcessor<>), CustomHandler = nameof(AddProcessor))] 931 | public static partial void AddProcessors(); 932 | 933 | private static void AddProcessor() 934 | where TProcessor : class, IProcessor 935 | where TValue : struct 936 | { 937 | } 938 | } 939 | """; 940 | 941 | var services = """ 942 | namespace GeneratorTests; 943 | 944 | public interface IProcessor { } 945 | 946 | public class IntProcessor : IProcessor { } 947 | public class StringProcessor : IProcessor { } 948 | public class GuidProcessor : IProcessor { } 949 | """; 950 | 951 | var compilation = CreateCompilation(source, services); 952 | 953 | var results = CSharpGeneratorDriver 954 | .Create(_generator) 955 | .RunGenerators(compilation) 956 | .GetRunResult(); 957 | 958 | var expected = """ 959 | namespace GeneratorTests; 960 | 961 | public static partial class ServiceCollectionExtensions 962 | { 963 | public static partial void AddProcessors() 964 | { 965 | AddProcessor(); 966 | AddProcessor(); 967 | } 968 | } 969 | """; 970 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 971 | } 972 | 973 | [Fact] 974 | public void CustomHandler_CombinedConstraints() 975 | { 976 | var source = """ 977 | using ServiceScan.SourceGenerator; 978 | 979 | namespace GeneratorTests; 980 | 981 | public interface IConfigurable { } 982 | 983 | public static partial class ServiceCollectionExtensions 984 | { 985 | [GenerateServiceRegistrations(AssignableTo = typeof(IHandler<>), CustomHandler = nameof(AddHandler))] 986 | public static partial void AddHandlers(); 987 | 988 | private static void AddHandler() 989 | where THandler : class, IHandler, IConfigurable, new() 990 | where TArg : class, new() 991 | { 992 | } 993 | } 994 | """; 995 | 996 | var services = """ 997 | namespace GeneratorTests; 998 | 999 | public interface IHandler { } 1000 | 1001 | public class Arg1 { } 1002 | public class Arg2 { public Arg2(int x) { } } 1003 | 1004 | public class ValidHandler : IHandler, IConfigurable { } 1005 | public class HandlerWithoutConfigurable : IHandler { } 1006 | public class HandlerWithoutConstructor : IHandler, IConfigurable 1007 | { 1008 | public HandlerWithoutConstructor(int x) { } 1009 | } 1010 | public class HandlerWithNonConstructibleArg : IHandler, IConfigurable { } 1011 | """; 1012 | 1013 | var compilation = CreateCompilation(source, services); 1014 | 1015 | var results = CSharpGeneratorDriver 1016 | .Create(_generator) 1017 | .RunGenerators(compilation) 1018 | .GetRunResult(); 1019 | 1020 | var expected = """ 1021 | namespace GeneratorTests; 1022 | 1023 | public static partial class ServiceCollectionExtensions 1024 | { 1025 | public static partial void AddHandlers() 1026 | { 1027 | AddHandler(); 1028 | } 1029 | } 1030 | """; 1031 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 1032 | } 1033 | 1034 | [Fact] 1035 | public void CustomHandler_HandlesRecursiveConstraints() 1036 | { 1037 | var source = """ 1038 | using ServiceScan.SourceGenerator; 1039 | 1040 | namespace GeneratorTests; 1041 | 1042 | public static partial class ServicesExtensions 1043 | { 1044 | [GenerateServiceRegistrations(TypeNameFilter = "*Smth*", CustomHandler = nameof(HandleType))] 1045 | public static partial void ProcessServices(); 1046 | 1047 | private static void HandleType() 1048 | where X : ISmth 1049 | where Y : ISmth 1050 | => System.Console.WriteLine(typeof(X).Name); 1051 | } 1052 | """; 1053 | 1054 | var services = """ 1055 | namespace GeneratorTests; 1056 | 1057 | interface ISmth; 1058 | class SmthX: ISmth; 1059 | class SmthY: ISmth; 1060 | class SmthString: ISmth; 1061 | """; 1062 | 1063 | var compilation = CreateCompilation(source, services); 1064 | 1065 | var results = CSharpGeneratorDriver 1066 | .Create(_generator) 1067 | .RunGenerators(compilation) 1068 | .GetRunResult(); 1069 | 1070 | var expected = """ 1071 | namespace GeneratorTests; 1072 | 1073 | public static partial class ServicesExtensions 1074 | { 1075 | public static partial void ProcessServices() 1076 | { 1077 | HandleType(); 1078 | HandleType(); 1079 | } 1080 | } 1081 | """; 1082 | Assert.Equal(expected, results.GeneratedTrees[1].ToString()); 1083 | } 1084 | 1085 | private static Compilation CreateCompilation(params string[] source) 1086 | { 1087 | var path = Path.GetDirectoryName(typeof(object).Assembly.Location)!; 1088 | var runtimeAssemblyPath = Path.Combine(path, "System.Runtime.dll"); 1089 | 1090 | var runtimeReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); 1091 | 1092 | return CSharpCompilation.Create("compilation", 1093 | source.Select(s => CSharpSyntaxTree.ParseText(s)), 1094 | [ 1095 | MetadataReference.CreateFromFile(typeof(object).Assembly.Location), 1096 | MetadataReference.CreateFromFile(runtimeAssemblyPath), 1097 | MetadataReference.CreateFromFile(typeof(IServiceCollection).Assembly.Location), 1098 | MetadataReference.CreateFromFile(typeof(External.IExternalService).Assembly.Location), 1099 | ], 1100 | new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); 1101 | } 1102 | } 1103 | -------------------------------------------------------------------------------- /ServiceScan.SourceGenerator.Tests/AddServicesTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Xunit; 5 | 6 | namespace ServiceScan.SourceGenerator.Tests; 7 | 8 | public class AddServicesTests 9 | { 10 | private readonly DependencyInjectionGenerator _generator = new(); 11 | 12 | [Theory] 13 | [InlineData(ServiceLifetime.Scoped)] 14 | [InlineData(ServiceLifetime.Transient)] 15 | [InlineData(ServiceLifetime.Singleton)] 16 | public void AddServicesWithLifetime(ServiceLifetime lifetime) 17 | { 18 | var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(IService), Lifetime = ServiceLifetime.{lifetime})]"; 19 | 20 | var compilation = CreateCompilation( 21 | Sources.MethodWithAttribute(attribute), 22 | """ 23 | namespace GeneratorTests; 24 | 25 | public interface IService { } 26 | public class MyService1 : IService { } 27 | public class MyService2 : IService { } 28 | """); 29 | 30 | var results = CSharpGeneratorDriver 31 | .Create(_generator) 32 | .RunGenerators(compilation) 33 | .GetRunResult(); 34 | 35 | var registrations = $""" 36 | return services 37 | .Add{lifetime}() 38 | .Add{lifetime}(); 39 | """; 40 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 41 | } 42 | 43 | [Fact] 44 | public void AddServicesFromAnotherAssembly() 45 | { 46 | var attribute = "[GenerateServiceRegistrations(FromAssemblyOf = typeof(External.IExternalService), AssignableTo = typeof(External.IExternalService))]"; 47 | var compilation = CreateCompilation(Sources.MethodWithAttribute(attribute)); 48 | 49 | var results = CSharpGeneratorDriver 50 | .Create(_generator) 51 | .RunGenerators(compilation) 52 | .GetRunResult(); 53 | 54 | var registrations = $""" 55 | return services 56 | .AddTransient() 57 | .AddTransient(); 58 | """; 59 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 60 | } 61 | 62 | [Fact] 63 | public void AddServicesFromReferencedAssembliesByAssemblyNameFilter() 64 | { 65 | var coreCompilation = CreateCompilation( 66 | """ 67 | namespace Core; 68 | public interface IService { } 69 | """) 70 | .WithAssemblyName("MyProduct.Core"); 71 | 72 | var implementation1Compilation = CreateCompilation([""" 73 | namespace Module1; 74 | public class MyService1 : Core.IService { } 75 | """], 76 | [coreCompilation]) 77 | .WithAssemblyName("MyProduct.Module1"); 78 | 79 | var implementation2Compilation = CreateCompilation([""" 80 | namespace Module2; 81 | public class MyService2 : Core.IService { } 82 | """], 83 | [coreCompilation]) 84 | .WithAssemblyName("MyProduct.Module2"); 85 | 86 | var attribute = """[GenerateServiceRegistrations(AssignableTo = typeof(Core.IService), Lifetime = ServiceLifetime.Scoped, AssemblyNameFilter="MyProduct.*")]"""; 87 | var registrationsCompilation = CreateCompilation( 88 | [Sources.MethodWithAttribute(attribute)], 89 | [coreCompilation, implementation1Compilation, implementation2Compilation]); 90 | 91 | var results = CSharpGeneratorDriver 92 | .Create(_generator) 93 | .RunGenerators(registrationsCompilation) 94 | .GetRunResult(); 95 | 96 | var registrations = $""" 97 | return services 98 | .AddScoped() 99 | .AddScoped(); 100 | """; 101 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 102 | } 103 | 104 | [Fact] 105 | public void AddServiceWithNonDirectInterface() 106 | { 107 | var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(IService))]"; 108 | 109 | var compilation = CreateCompilation( 110 | Sources.MethodWithAttribute(attribute), 111 | """ 112 | namespace GeneratorTests; 113 | 114 | public interface IService { } 115 | public abstract class AbstractService : IService { } 116 | public class MyService1 : AbstractService { } 117 | public class MyService2 : AbstractService { } 118 | """); 119 | 120 | var results = CSharpGeneratorDriver 121 | .Create(_generator) 122 | .RunGenerators(compilation) 123 | .GetRunResult(); 124 | 125 | var registrations = $""" 126 | return services 127 | .AddTransient() 128 | .AddTransient(); 129 | """; 130 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 131 | } 132 | 133 | [Fact] 134 | public void AddServiceWithNonDirectAbstractClass() 135 | { 136 | var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(BaseType))]"; 137 | 138 | var compilation = CreateCompilation( 139 | Sources.MethodWithAttribute(attribute), 140 | """ 141 | namespace GeneratorTests; 142 | 143 | public abstract class BaseType { } 144 | public abstract class AbstractService : BaseType { } 145 | public class MyService1 : AbstractService { } 146 | public class MyService2 : AbstractService { } 147 | """); 148 | 149 | var results = CSharpGeneratorDriver 150 | .Create(_generator) 151 | .RunGenerators(compilation) 152 | .GetRunResult(); 153 | 154 | var registrations = $""" 155 | return services 156 | .AddTransient() 157 | .AddTransient(); 158 | """; 159 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 160 | } 161 | 162 | [Fact] 163 | public void AddServicesAssignableToOpenGenericInterface() 164 | { 165 | var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(IService<>))]"; 166 | 167 | var compilation = CreateCompilation( 168 | Sources.MethodWithAttribute(attribute), 169 | """ 170 | namespace GeneratorTests; 171 | 172 | public interface IService { } 173 | public class MyIntService : IService { } 174 | public class MyStringService : IService { } 175 | """); 176 | 177 | var results = CSharpGeneratorDriver 178 | .Create(_generator) 179 | .RunGenerators(compilation) 180 | .GetRunResult(); 181 | 182 | var registrations = $""" 183 | return services 184 | .AddTransient, global::GeneratorTests.MyIntService>() 185 | .AddTransient, global::GeneratorTests.MyStringService>(); 186 | """; 187 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 188 | } 189 | 190 | [Fact] 191 | public void AddServicesAssignableToOpenGenericInterface_WithMultipleInterfaces() 192 | { 193 | var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(IService<>))]"; 194 | 195 | var compilation = CreateCompilation( 196 | Sources.MethodWithAttribute(attribute), 197 | """ 198 | namespace GeneratorTests; 199 | 200 | public interface IService { } 201 | public interface IOtherInterface { } 202 | public class MyIntAndStringService : IService, IService, IOtherInterface { } 203 | """); 204 | 205 | var results = CSharpGeneratorDriver 206 | .Create(_generator) 207 | .RunGenerators(compilation) 208 | .GetRunResult(); 209 | 210 | var registrations = $""" 211 | return services 212 | .AddTransient, global::GeneratorTests.MyIntAndStringService>() 213 | .AddTransient, global::GeneratorTests.MyIntAndStringService>(); 214 | """; 215 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 216 | } 217 | 218 | [Fact] 219 | public void AddServicesAssignableToOpenGenericInterface_WithMultipleInterfaces_AsSelfAndAsImplementedInterfaces() 220 | { 221 | var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(IService<>), AsSelf = true, AsImplementedInterfaces = true, Lifetime = ServiceLifetime.Singleton)]"; 222 | 223 | var compilation = CreateCompilation( 224 | Sources.MethodWithAttribute(attribute), 225 | """ 226 | namespace GeneratorTests; 227 | 228 | public interface IService { } 229 | public class MyIntAndStringService : IService, IService { } 230 | """); 231 | 232 | var results = CSharpGeneratorDriver 233 | .Create(_generator) 234 | .RunGenerators(compilation) 235 | .GetRunResult(); 236 | 237 | var registrations = $""" 238 | return services 239 | .AddSingleton() 240 | .AddSingleton>(s => s.GetRequiredService()) 241 | .AddSingleton>(s => s.GetRequiredService()); 242 | """; 243 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 244 | } 245 | 246 | [Fact] 247 | public void AddServicesAssignableToClosedGenericInterface() 248 | { 249 | var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(IService))]"; 250 | 251 | var compilation = CreateCompilation( 252 | Sources.MethodWithAttribute(attribute), 253 | """ 254 | namespace GeneratorTests; 255 | 256 | public interface IService { } 257 | public class MyIntService : IService { } 258 | public class MyStringService : IService { } 259 | """); 260 | 261 | var results = CSharpGeneratorDriver 262 | .Create(_generator) 263 | .RunGenerators(compilation) 264 | .GetRunResult(); 265 | 266 | var registrations = $""" 267 | return services 268 | .AddTransient, global::GeneratorTests.MyIntService>(); 269 | """; 270 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 271 | } 272 | 273 | [Fact] 274 | public void AddServicesAssignableToAbstractClass() 275 | { 276 | var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(AbstractService))]"; 277 | 278 | var compilation = CreateCompilation( 279 | Sources.MethodWithAttribute(attribute), 280 | """ 281 | namespace GeneratorTests; 282 | 283 | public abstract class AbstractService { } 284 | public class MyService1 : AbstractService { } 285 | public class MyService2 : AbstractService { } 286 | """); 287 | 288 | var results = CSharpGeneratorDriver 289 | .Create(_generator) 290 | .RunGenerators(compilation) 291 | .GetRunResult(); 292 | 293 | var registrations = $""" 294 | return services 295 | .AddTransient() 296 | .AddTransient(); 297 | """; 298 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 299 | } 300 | 301 | [Fact] 302 | public void AddServicesAssignableToAbstractClassAsSelf() 303 | { 304 | var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(AbstractService), AsSelf = true)]"; 305 | 306 | var compilation = CreateCompilation( 307 | Sources.MethodWithAttribute(attribute), 308 | """ 309 | namespace GeneratorTests; 310 | 311 | public abstract class AbstractService { } 312 | public class MyService1 : AbstractService { } 313 | public class MyService2 : AbstractService { } 314 | """); 315 | 316 | var results = CSharpGeneratorDriver 317 | .Create(_generator) 318 | .RunGenerators(compilation) 319 | .GetRunResult(); 320 | 321 | var registrations = $""" 322 | return services 323 | .AddTransient() 324 | .AddTransient(); 325 | """; 326 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 327 | } 328 | 329 | [Fact] 330 | public void AddServiceAssignableToSelf() 331 | { 332 | var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(MyService))]"; 333 | 334 | var compilation = CreateCompilation( 335 | Sources.MethodWithAttribute(attribute), 336 | """ 337 | namespace GeneratorTests; 338 | 339 | public class MyService { } 340 | """); 341 | 342 | var results = CSharpGeneratorDriver 343 | .Create(_generator) 344 | .RunGenerators(compilation) 345 | .GetRunResult(); 346 | 347 | var registrations = $""" 348 | return services 349 | .AddTransient(); 350 | """; 351 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 352 | } 353 | 354 | [Fact] 355 | public void AddServicesAssignableToOpenGenericAbstractClass() 356 | { 357 | var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(AbstractService<>))]"; 358 | 359 | var compilation = CreateCompilation( 360 | Sources.MethodWithAttribute(attribute), 361 | """ 362 | namespace GeneratorTests; 363 | 364 | public abstract class AbstractService { } 365 | public class MyIntService : AbstractService { } 366 | public class MyStringService : AbstractService { } 367 | """); 368 | 369 | var results = CSharpGeneratorDriver 370 | .Create(_generator) 371 | .RunGenerators(compilation) 372 | .GetRunResult(); 373 | 374 | var registrations = $""" 375 | return services 376 | .AddTransient, global::GeneratorTests.MyIntService>() 377 | .AddTransient, global::GeneratorTests.MyStringService>(); 378 | """; 379 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 380 | } 381 | 382 | [Fact] 383 | public void AddGenericServicesImplementingGenericInterfaceAsOpenGenerics() 384 | { 385 | var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(IGenericService<>))]"; 386 | 387 | var compilation = CreateCompilation( 388 | Sources.MethodWithAttribute(attribute), 389 | """ 390 | namespace GeneratorTests; 391 | 392 | public interface IGenericService { } 393 | public class MyService1 : IGenericService { } 394 | public class MyService2 : IGenericService { } 395 | """); 396 | 397 | var results = CSharpGeneratorDriver 398 | .Create(_generator) 399 | .RunGenerators(compilation) 400 | .GetRunResult(); 401 | 402 | var registrations = $""" 403 | return services 404 | .AddTransient(typeof(global::GeneratorTests.IGenericService<>), typeof(global::GeneratorTests.MyService1<>)) 405 | .AddTransient(typeof(global::GeneratorTests.IGenericService<>), typeof(global::GeneratorTests.MyService2<>)); 406 | """; 407 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 408 | } 409 | 410 | [Fact] 411 | public void AddGenericServicesImplementingNonGenericInterfaceAsOpenGenerics() 412 | { 413 | var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(IService))]"; 414 | 415 | var compilation = CreateCompilation( 416 | Sources.MethodWithAttribute(attribute), 417 | """ 418 | namespace GeneratorTests; 419 | 420 | public interface IService { } 421 | public class MyService1 : IService { } 422 | public class MyService2 : IService { } 423 | """); 424 | 425 | var results = CSharpGeneratorDriver 426 | .Create(_generator) 427 | .RunGenerators(compilation) 428 | .GetRunResult(); 429 | 430 | var registrations = $""" 431 | return services 432 | .AddTransient(typeof(global::GeneratorTests.IService), typeof(global::GeneratorTests.MyService1<>)) 433 | .AddTransient(typeof(global::GeneratorTests.IService), typeof(global::GeneratorTests.MyService2<>)); 434 | """; 435 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 436 | } 437 | 438 | [Fact] 439 | public void AddServicesWithTypeNameFilter() 440 | { 441 | 442 | var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service"))]"""; 443 | 444 | var compilation = CreateCompilation( 445 | Sources.MethodWithAttribute(attribute), 446 | """ 447 | namespace GeneratorTests; 448 | 449 | public class MyFirstService {} 450 | public class MySecondService {} 451 | public class ServiceWithNonMatchingName {} 452 | """); 453 | 454 | var results = CSharpGeneratorDriver 455 | .Create(_generator) 456 | .RunGenerators(compilation) 457 | .GetRunResult(); 458 | 459 | var registrations = $""" 460 | return services 461 | .AddTransient() 462 | .AddTransient(); 463 | """; 464 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 465 | } 466 | 467 | [Fact] 468 | public void AddServicesAttributeFilterFilter() 469 | { 470 | var attribute = """[GenerateServiceRegistrations(AttributeFilter = typeof(ServiceAttribute))]"""; 471 | 472 | var compilation = CreateCompilation( 473 | Sources.MethodWithAttribute(attribute), 474 | """ 475 | using System; 476 | 477 | namespace GeneratorTests; 478 | 479 | [AttributeUsage(AttributeTargets.Class)] 480 | public sealed class ServiceAttribute : Attribute; 481 | 482 | [Service] 483 | public class MyFirstService {} 484 | 485 | [Service] 486 | public class MySecondService {} 487 | 488 | public class ServiceWithoutAttribute {} 489 | """); 490 | 491 | var results = CSharpGeneratorDriver 492 | .Create(_generator) 493 | .RunGenerators(compilation) 494 | .GetRunResult(); 495 | 496 | var registrations = $""" 497 | return services 498 | .AddTransient() 499 | .AddTransient(); 500 | """; 501 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 502 | } 503 | 504 | [Fact] 505 | public void AddServicesAttributeFilterFilterAndTypeNameFilter() 506 | { 507 | var attribute = """[GenerateServiceRegistrations(AttributeFilter = typeof(ServiceAttribute), TypeNameFilter = "*Service")]"""; 508 | 509 | var compilation = CreateCompilation( 510 | Sources.MethodWithAttribute(attribute), 511 | """ 512 | using System; 513 | 514 | namespace GeneratorTests; 515 | 516 | [AttributeUsage(AttributeTargets.Class)] 517 | public sealed class ServiceAttribute : Attribute; 518 | 519 | [Service] 520 | public class MyFirstService {} 521 | 522 | public class MySecondServiceWithoutAttribute {} 523 | 524 | public class ServiceWithNonMatchingName {} 525 | """); 526 | 527 | var results = CSharpGeneratorDriver 528 | .Create(_generator) 529 | .RunGenerators(compilation) 530 | .GetRunResult(); 531 | 532 | var registrations = $""" 533 | return services 534 | .AddTransient(); 535 | """; 536 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 537 | } 538 | 539 | [Fact] 540 | public void AddServicesWithTypeNameFilter_MultipleGroups() 541 | { 542 | 543 | var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*First*,*Second*"))]"""; 544 | 545 | var compilation = CreateCompilation( 546 | Sources.MethodWithAttribute(attribute), 547 | """ 548 | namespace GeneratorTests; 549 | 550 | public class MyFirstService {} 551 | public class MySecondService {} 552 | public class ServiceWithNonMatchingName {} 553 | """); 554 | 555 | var results = CSharpGeneratorDriver 556 | .Create(_generator) 557 | .RunGenerators(compilation) 558 | .GetRunResult(); 559 | 560 | var registrations = $""" 561 | return services 562 | .AddTransient() 563 | .AddTransient(); 564 | """; 565 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 566 | } 567 | 568 | [Fact] 569 | public void AddServices_ExcludeByTypeName() 570 | { 571 | var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", ExcludeByTypeName = "*Second*")]"""; 572 | 573 | var compilation = CreateCompilation( 574 | Sources.MethodWithAttribute(attribute), 575 | """ 576 | namespace GeneratorTests; 577 | 578 | public class MyFirstService {} 579 | public class MySecondService {} 580 | public class ThirdService {} 581 | """); 582 | 583 | var results = CSharpGeneratorDriver 584 | .Create(_generator) 585 | .RunGenerators(compilation) 586 | .GetRunResult(); 587 | 588 | var registrations = $""" 589 | return services 590 | .AddTransient() 591 | .AddTransient(); 592 | """; 593 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 594 | } 595 | 596 | [Fact] 597 | public void AddServices_ExcludeByAttribute() 598 | { 599 | var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", ExcludeByAttribute = typeof(ExcludeAttribute))]"""; 600 | 601 | var compilation = CreateCompilation( 602 | Sources.MethodWithAttribute(attribute), 603 | """ 604 | using System; 605 | 606 | namespace GeneratorTests; 607 | 608 | [AttributeUsage(AttributeTargets.Class)] 609 | public sealed class ExcludeAttribute : Attribute; 610 | 611 | public class MyFirstService {} 612 | 613 | [Exclude] 614 | public class MySecondService {} 615 | 616 | public class ThirdService {} 617 | """); 618 | 619 | var results = CSharpGeneratorDriver 620 | .Create(_generator) 621 | .RunGenerators(compilation) 622 | .GetRunResult(); 623 | 624 | var registrations = $""" 625 | return services 626 | .AddTransient() 627 | .AddTransient(); 628 | """; 629 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 630 | } 631 | 632 | [Fact] 633 | public void AddServices_ExcludeByTypeNameAndAttribute() 634 | { 635 | var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", ExcludeByTypeName = "*Third*", ExcludeByAttribute = typeof(ExcludeAttribute))]"""; 636 | 637 | var compilation = CreateCompilation( 638 | Sources.MethodWithAttribute(attribute), 639 | """ 640 | using System; 641 | 642 | namespace GeneratorTests; 643 | 644 | [AttributeUsage(AttributeTargets.Class)] 645 | public sealed class ExcludeAttribute : Attribute; 646 | 647 | public class MyFirstService {} 648 | 649 | [Exclude] 650 | public class MySecondService {} 651 | 652 | public class ThirdService {} 653 | 654 | public class FourthService {} 655 | """); 656 | 657 | var results = CSharpGeneratorDriver 658 | .Create(_generator) 659 | .RunGenerators(compilation) 660 | .GetRunResult(); 661 | 662 | var registrations = $""" 663 | return services 664 | .AddTransient() 665 | .AddTransient(); 666 | """; 667 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 668 | } 669 | 670 | [Fact] 671 | public void AddServices_ExcludeAssignableTo_Interface() 672 | { 673 | var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", ExcludeAssignableTo = typeof(IExclude))]"""; 674 | 675 | var compilation = CreateCompilation( 676 | Sources.MethodWithAttribute(attribute), 677 | """ 678 | namespace GeneratorTests; 679 | 680 | public interface IExclude {} 681 | 682 | public class MyFirstService {} 683 | 684 | public class MySecondService : IExclude {} 685 | 686 | public class ThirdService {} 687 | """); 688 | 689 | var results = CSharpGeneratorDriver 690 | .Create(_generator) 691 | .RunGenerators(compilation) 692 | .GetRunResult(); 693 | 694 | var registrations = $""" 695 | return services 696 | .AddTransient() 697 | .AddTransient(); 698 | """; 699 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 700 | } 701 | 702 | [Fact] 703 | public void AddServices_ExcludeAssignableTo_AbstractClass() 704 | { 705 | var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", ExcludeAssignableTo = typeof(ExcludeBase))]"""; 706 | 707 | var compilation = CreateCompilation( 708 | Sources.MethodWithAttribute(attribute), 709 | """ 710 | namespace GeneratorTests; 711 | 712 | public abstract class ExcludeBase {} 713 | 714 | public class MyFirstService {} 715 | 716 | public class MySecondService : ExcludeBase {} 717 | 718 | public class ThirdService {} 719 | """); 720 | 721 | var results = CSharpGeneratorDriver 722 | .Create(_generator) 723 | .RunGenerators(compilation) 724 | .GetRunResult(); 725 | 726 | var registrations = $""" 727 | return services 728 | .AddTransient() 729 | .AddTransient(); 730 | """; 731 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 732 | } 733 | 734 | [Fact] 735 | public void AddServices_ExcludeAssignableTo_OpenGenericInterface() 736 | { 737 | var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", ExcludeAssignableTo = typeof(IExclude<>))]"""; 738 | 739 | var compilation = CreateCompilation( 740 | Sources.MethodWithAttribute(attribute), 741 | """ 742 | namespace GeneratorTests; 743 | 744 | public interface IExclude {} 745 | 746 | public class MyFirstService {} 747 | 748 | public class MySecondService : IExclude {} 749 | 750 | public class ThirdService : IExclude {} 751 | 752 | public class FourthService {} 753 | """); 754 | 755 | var results = CSharpGeneratorDriver 756 | .Create(_generator) 757 | .RunGenerators(compilation) 758 | .GetRunResult(); 759 | 760 | var registrations = $""" 761 | return services 762 | .AddTransient() 763 | .AddTransient(); 764 | """; 765 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 766 | } 767 | 768 | [Fact] 769 | public void AddServices_ExcludeAssignableTo_ClosedGenericInterface() 770 | { 771 | var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", ExcludeAssignableTo = typeof(IExclude))]"""; 772 | 773 | var compilation = CreateCompilation( 774 | Sources.MethodWithAttribute(attribute), 775 | """ 776 | namespace GeneratorTests; 777 | 778 | public interface IExclude {} 779 | 780 | public class MyFirstService {} 781 | 782 | public class MySecondService : IExclude {} 783 | 784 | public class ThirdService : IExclude {} 785 | 786 | public class FourthService {} 787 | """); 788 | 789 | var results = CSharpGeneratorDriver 790 | .Create(_generator) 791 | .RunGenerators(compilation) 792 | .GetRunResult(); 793 | 794 | var registrations = $""" 795 | return services 796 | .AddTransient() 797 | .AddTransient() 798 | .AddTransient(); 799 | """; 800 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 801 | } 802 | 803 | [Fact] 804 | public void AddServices_AssignableToAndExcludeAssignableTo() 805 | { 806 | var attribute = """[GenerateServiceRegistrations(AssignableTo = typeof(IService), ExcludeAssignableTo = typeof(IExclude))]"""; 807 | 808 | var compilation = CreateCompilation( 809 | Sources.MethodWithAttribute(attribute), 810 | """ 811 | namespace GeneratorTests; 812 | 813 | public interface IService { } 814 | public interface IExclude { } 815 | 816 | public class MyFirstService : IService { } 817 | public class MySecondService : IService, IExclude { } 818 | public class MyThirdService : IService { } 819 | """); 820 | 821 | var results = CSharpGeneratorDriver 822 | .Create(_generator) 823 | .RunGenerators(compilation) 824 | .GetRunResult(); 825 | 826 | var registrations = $""" 827 | return services 828 | .AddTransient() 829 | .AddTransient(); 830 | """; 831 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 832 | } 833 | 834 | [Fact] 835 | public void AddServicesWithTypeNameFilterAsImplementedInterfaces() 836 | { 837 | var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", AsImplementedInterfaces = true))]"""; 838 | 839 | var compilation = CreateCompilation( 840 | Sources.MethodWithAttribute(attribute), 841 | """ 842 | namespace GeneratorTests; 843 | 844 | public interface IServiceA {} 845 | public interface IServiceB {} 846 | public interface IServiceC {} 847 | public class MyFirstService: IServiceA {} 848 | public class MySecondService: IServiceB, IServiceC {} 849 | public class InterfacelessService {} 850 | """); 851 | 852 | var results = CSharpGeneratorDriver 853 | .Create(_generator) 854 | .RunGenerators(compilation) 855 | .GetRunResult(); 856 | 857 | var registrations = $""" 858 | return services 859 | .AddTransient() 860 | .AddTransient() 861 | .AddTransient(); 862 | """; 863 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 864 | } 865 | 866 | [Fact] 867 | public void AddServicesBothAsSelfAndAsImplementedInterfaces_InterfacesAreForwardedToSelfRegistration() 868 | { 869 | var attribute = """ 870 | [GenerateServiceRegistrations( 871 | TypeNameFilter = "*Service", 872 | AsImplementedInterfaces = true, 873 | AsSelf = true, 874 | Lifetime = ServiceLifetime.Singleton))] 875 | """; 876 | 877 | var compilation = CreateCompilation( 878 | Sources.MethodWithAttribute(attribute), 879 | """ 880 | namespace GeneratorTests; 881 | 882 | public interface IServiceA {} 883 | public interface IServiceB {} 884 | public class MyService: IServiceA, IServiceB {} 885 | """); 886 | 887 | var results = CSharpGeneratorDriver 888 | .Create(_generator) 889 | .RunGenerators(compilation) 890 | .GetRunResult(); 891 | 892 | var registrations = $""" 893 | return services 894 | .AddSingleton() 895 | .AddSingleton(s => s.GetRequiredService()) 896 | .AddSingleton(s => s.GetRequiredService()); 897 | """; 898 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 899 | } 900 | 901 | [Fact] 902 | public void IDisposableServiceAreExcludedWithImplementedInterfaces() 903 | { 904 | var attribute = """[GenerateServiceRegistrations(TypeNameFilter = "*Service", AsImplementedInterfaces = true))]"""; 905 | 906 | var compilation = CreateCompilation( 907 | Sources.MethodWithAttribute(attribute), 908 | """ 909 | namespace GeneratorTests; 910 | 911 | public interface IServiceA {} 912 | public interface IServiceB {} 913 | public interface IServiceC {} 914 | 915 | public class MyFirstService: IServiceA, System.IDisposable 916 | { 917 | public void Dispose() {} 918 | } 919 | 920 | public class MySecondService: IServiceB, IServiceC, System.IDisposable 921 | { 922 | public void Dispose() {} 923 | } 924 | """); 925 | 926 | var results = CSharpGeneratorDriver 927 | .Create(_generator) 928 | .RunGenerators(compilation) 929 | .GetRunResult(); 930 | 931 | var registrations = $""" 932 | return services 933 | .AddTransient() 934 | .AddTransient() 935 | .AddTransient(); 936 | """; 937 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 938 | } 939 | 940 | [Fact] 941 | public void AddNestedTypes() 942 | { 943 | var attribute = "[GenerateServiceRegistrations(AssignableTo = typeof(IService))]"; 944 | var compilation = CreateCompilation(Sources.MethodWithAttribute(attribute), 945 | """ 946 | namespace GeneratorTests; 947 | 948 | public interface IService { } 949 | 950 | public class ParentType1 951 | { 952 | public class MyService1 : IService { } 953 | public class MyService2 : IService { } 954 | } 955 | 956 | public class ParentType2 957 | { 958 | public class MyService1 : IService { } 959 | private class NestedPrivateService : IService { } // Shouldn't be added as non-accessible 960 | } 961 | """); 962 | 963 | var results = CSharpGeneratorDriver 964 | .Create(_generator) 965 | .RunGenerators(compilation) 966 | .GetRunResult(); 967 | 968 | var registrations = $""" 969 | return services 970 | .AddTransient() 971 | .AddTransient() 972 | .AddTransient(); 973 | """; 974 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 975 | } 976 | 977 | [Fact] 978 | public void AddAsKeyedServices_GenericMethod() 979 | { 980 | var attribute = @" 981 | private static string GetName() => typeof(T).Name.Replace(""Service"", """"); 982 | 983 | [GenerateServiceRegistrations(AssignableTo = typeof(IService), KeySelector = nameof(GetName))]"; 984 | 985 | var compilation = CreateCompilation( 986 | Sources.MethodWithAttribute(attribute), 987 | """ 988 | namespace GeneratorTests; 989 | 990 | public interface IService { } 991 | public class MyService1 : IService { } 992 | public class MyService2 : IService { } 993 | """); 994 | 995 | var results = CSharpGeneratorDriver 996 | .Create(_generator) 997 | .RunGenerators(compilation) 998 | .GetRunResult(); 999 | 1000 | var registrations = $""" 1001 | return services 1002 | .AddKeyedTransient(GetName()) 1003 | .AddKeyedTransient(GetName()); 1004 | """; 1005 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 1006 | } 1007 | 1008 | [Fact] 1009 | public void AddAsKeyedServices_MethodWithTypeParameter() 1010 | { 1011 | var attribute = @" 1012 | private static string GetName(Type type) => type.Name.Replace(""Service"", """"); 1013 | 1014 | [GenerateServiceRegistrations(AssignableTo = typeof(IService), KeySelector = nameof(GetName))]"; 1015 | 1016 | var compilation = CreateCompilation( 1017 | Sources.MethodWithAttribute(attribute), 1018 | """ 1019 | namespace GeneratorTests; 1020 | 1021 | public interface IService { } 1022 | public class MyService1 : IService { } 1023 | public class MyService2 : IService { } 1024 | """); 1025 | 1026 | var results = CSharpGeneratorDriver 1027 | .Create(_generator) 1028 | .RunGenerators(compilation) 1029 | .GetRunResult(); 1030 | 1031 | var registrations = $""" 1032 | return services 1033 | .AddKeyedTransient(GetName(typeof(global::GeneratorTests.MyService1))) 1034 | .AddKeyedTransient(GetName(typeof(global::GeneratorTests.MyService2))); 1035 | """; 1036 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 1037 | } 1038 | 1039 | [Fact] 1040 | public void AddAsKeyedServices_ConstantFieldInType() 1041 | { 1042 | var attribute = @"[GenerateServiceRegistrations(AssignableTo = typeof(IService), KeySelector = ""Key"")]"; 1043 | 1044 | var compilation = CreateCompilation( 1045 | Sources.MethodWithAttribute(attribute), 1046 | """ 1047 | namespace GeneratorTests; 1048 | 1049 | public interface IService { } 1050 | 1051 | public class MyService1 : IService 1052 | { 1053 | public const string Key = "MSR1"; 1054 | } 1055 | 1056 | public class MyService2 : IService 1057 | { 1058 | public const string Key = "MSR2"; 1059 | } 1060 | """); 1061 | 1062 | var results = CSharpGeneratorDriver 1063 | .Create(_generator) 1064 | .RunGenerators(compilation) 1065 | .GetRunResult(); 1066 | 1067 | var registrations = $""" 1068 | return services 1069 | .AddKeyedTransient(global::GeneratorTests.MyService1.Key) 1070 | .AddKeyedTransient(global::GeneratorTests.MyService2.Key); 1071 | """; 1072 | Assert.Equal(Sources.GetMethodImplementation(registrations), results.GeneratedTrees[1].ToString()); 1073 | } 1074 | 1075 | [Fact] 1076 | public void DontGenerateAnythingIfTypeIsInvalid() 1077 | { 1078 | var attribute = $"[GenerateServiceRegistrations(AssignableTo = typeof(IWrongService))]"; 1079 | 1080 | var compilation = CreateCompilation(Sources.MethodWithAttribute(attribute)); 1081 | 1082 | var results = CSharpGeneratorDriver 1083 | .Create(_generator) 1084 | .RunGenerators(compilation) 1085 | .GetRunResult(); 1086 | 1087 | // One file for generated attribute itself. 1088 | Assert.Single(results.GeneratedTrees); 1089 | } 1090 | 1091 | private static Compilation CreateCompilation(params string[] source) 1092 | { 1093 | return CreateCompilation(source, []); 1094 | } 1095 | 1096 | private static Compilation CreateCompilation(string[] source, Compilation[] referencedCompilations) 1097 | { 1098 | var path = Path.GetDirectoryName(typeof(object).Assembly.Location)!; 1099 | var runtimeAssemblyPath = Path.Combine(path, "System.Runtime.dll"); 1100 | 1101 | var runtimeReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); 1102 | 1103 | var metadataReferences = new MetadataReference[] 1104 | { 1105 | MetadataReference.CreateFromFile(typeof(object).Assembly.Location), 1106 | MetadataReference.CreateFromFile(runtimeAssemblyPath), 1107 | MetadataReference.CreateFromFile(typeof(IServiceCollection).Assembly.Location), 1108 | MetadataReference.CreateFromFile(typeof(External.IExternalService).Assembly.Location) 1109 | } 1110 | .Concat(referencedCompilations.Select(c => c.ToMetadataReference())); 1111 | 1112 | return CSharpCompilation.Create("compilation", 1113 | source.Select(s => CSharpSyntaxTree.ParseText(s)), 1114 | metadataReferences, 1115 | new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); 1116 | } 1117 | } 1118 | 1119 | --------------------------------------------------------------------------------