From a15f2e67fef4ebafbcaf1434f5a18d7aa1816c96 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 5 Nov 2024 12:55:06 -0600 Subject: [PATCH 1/4] Added source generator for actor registration that will register any type that implements Dapr.Actors.Runtime.Actor Signed-off-by: Whit Waldo --- .../ActorRegistrationGenerator.cs | 101 ++++++++++++++++++ .../ActorRegistrationGeneratorTests.cs | 79 ++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs create mode 100644 test/Dapr.Actors.Generators.Test/ActorRegistrationGeneratorTests.cs diff --git a/src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs b/src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs new file mode 100644 index 000000000..158a7f46a --- /dev/null +++ b/src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs @@ -0,0 +1,101 @@ +// ------------------------------------------------------------------------ +// Copyright 2023 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Dapr.Actors.Generators; + +/// +/// Generates an extension method that can be used during dependency injection to register all actor types. +/// +[Generator] +public sealed class ActorRegistrationGenerator : IIncrementalGenerator +{ + /// + /// Initializes the generator and registers the syntax receiver. + /// + /// The to register callbacks on + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var classDeclarations = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (s, _) => IsClassDeclaration(s), + transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx)) + .Where(static m => m is not null); + + var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect()); + context.RegisterSourceOutput(compilationAndClasses, static (spc, source) => Execute(source.Right, spc)); + } + + private static bool IsClassDeclaration(SyntaxNode node) => node is ClassDeclarationSyntax; + + private static INamedTypeSymbol? GetSemanticTargetForGeneration(GeneratorSyntaxContext context) + { + var classDeclaration = (ClassDeclarationSyntax)context.Node; + var model = context.SemanticModel; + + if (model.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol classSymbol) + { + return null; + } + + var actorClass = context.SemanticModel.Compilation.GetTypeByMetadataName("Dapr.Actors.Runtime.Actor"); + return classSymbol.BaseType != null && classSymbol.BaseType.Equals(actorClass, SymbolEqualityComparer.Default) ? classSymbol : null; + } + + private static void Execute(ImmutableArray actorTypes, + SourceProductionContext context) + { + var validActorTypes = actorTypes.Where(static t => t is not null).Cast().ToList(); + var source = GenerateActorRegistrationSource(validActorTypes); + context.AddSource("ActorRegistrationExtensions.g.cs", SourceText.From(source, Encoding.UTF8)); + } + + /// + /// Generates the source code for the actor registration method. + /// + /// The list of actor types to register. + /// The generated source code as a string. + private static string GenerateActorRegistrationSource(IReadOnlyList actorTypes) + { +#pragma warning disable RS1035 + var registrations = string.Join(Environment.NewLine, +#pragma warning restore RS1035 + actorTypes.Select(t => $"options.Actors.RegisterActor<{t.ToDisplayString()}>();")); + + return $@" +using Microsoft.Extensions.DependencyInjection; +using Dapr.Actors.Runtime; + +/// +/// Extension methods for registering Dapr actors. +/// +public static class ActorRegistrationExtensions +{{ + /// + /// Registers all discovered actor types with the Dapr actor runtime. + /// + public static void RegisterAllActors(this IServiceCollection services) + {{ + services.AddActors(options => + {{ + {registrations} + }}); + }} +}}"; + } +} diff --git a/test/Dapr.Actors.Generators.Test/ActorRegistrationGeneratorTests.cs b/test/Dapr.Actors.Generators.Test/ActorRegistrationGeneratorTests.cs new file mode 100644 index 000000000..c77927f1b --- /dev/null +++ b/test/Dapr.Actors.Generators.Test/ActorRegistrationGeneratorTests.cs @@ -0,0 +1,79 @@ +using System.Text; +using Dapr.Actors.Runtime; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Extensions.DependencyInjection; + +namespace Dapr.Actors.Generators.Test; + +public class ActorRegistrationGeneratorTests +{ + [Fact] + public void TestActorRegistrationGenerator() + { + const string source = @" +using Dapr.Actors.Runtime; + +public class MyActor : Actor, IMyActor +{ + public MyActor(ActorHost host) : base(host) { } +} + +public interface IMyActor : IActor +{ +} +"; + + const string expectedGeneratedCode = @" +using Microsoft.Extensions.DependencyInjection; +using Dapr.Actors.Runtime; + +/// +/// Extension methods for registering Dapr actors. +/// +public static class ActorRegistrationExtensions +{ + /// + /// Registers all discovered actor types with the Dapr actor runtime. + /// + public static void RegisterAllActors(this IServiceCollection services) + { + services.AddActors(options => + { + options.Actors.RegisterActor(); + }); + } +}"; + + var generatedCode = GetGeneratedCode(source); + Assert.Equal(expectedGeneratedCode.Trim(), generatedCode.Trim()); + } + + private static string GetGeneratedCode(string source) + { + var syntaxTree = CSharpSyntaxTree.ParseText(SourceText.From(source, Encoding.UTF8)); + var references = new List + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Actor).Assembly.Location), + MetadataReference.CreateFromFile(typeof(IServiceCollection).Assembly.Location) + }; + + var compilation = CSharpCompilation.Create("TestCompilation", + new[] { syntaxTree }, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + var generator = new ActorRegistrationGenerator(); + var driver = CSharpGeneratorDriver.Create(generator); + driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); + + var generatedTrees = outputCompilation.SyntaxTrees.Skip(1).ToList(); + Assert.Single(generatedTrees); + + var generatedCode = generatedTrees[0].ToString(); + return generatedCode; + } +} From 21ada186a00d6a67a42cc72ae5cb29aba25108ee Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Tue, 5 Nov 2024 14:01:40 -0600 Subject: [PATCH 2/4] Added flag to the registration extension to optionally register actors from transient project references Signed-off-by: Whit Waldo --- .../ActorRegistrationGenerator.cs | 49 +++++++++-- .../Extensions/IEnumerableExtensions.cs | 45 +++++----- .../Extensions/INamespaceSymbolExtensions.cs | 33 ++++++++ .../ActorRegistrationGeneratorTests.cs | 83 ++++++++++++++++++- .../Extensions/INamespaceExtensionsTests.cs | 40 +++++++++ 5 files changed, 218 insertions(+), 32 deletions(-) create mode 100644 src/Dapr.Actors.Generators/Extensions/INamespaceSymbolExtensions.cs create mode 100644 test/Dapr.Actors.Generators.Test/Extensions/INamespaceExtensionsTests.cs diff --git a/src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs b/src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs index 158a7f46a..4df130882 100644 --- a/src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs +++ b/src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs @@ -13,10 +13,13 @@ using System.Collections.Immutable; using System.Text; +using Dapr.Actors.Generators.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; + + namespace Dapr.Actors.Generators; /// @@ -25,6 +28,8 @@ namespace Dapr.Actors.Generators; [Generator] public sealed class ActorRegistrationGenerator : IIncrementalGenerator { + private const string DaprActorType = "Dapr.Actors.Runtime.Actor"; + /// /// Initializes the generator and registers the syntax receiver. /// @@ -38,7 +43,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Where(static m => m is not null); var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect()); - context.RegisterSourceOutput(compilationAndClasses, static (spc, source) => Execute(source.Right, spc)); + context.RegisterSourceOutput(compilationAndClasses, static (spc, source) => Execute(source.Left, source.Right, spc)); } private static bool IsClassDeclaration(SyntaxNode node) => node is ClassDeclarationSyntax; @@ -53,24 +58,25 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return null; } - var actorClass = context.SemanticModel.Compilation.GetTypeByMetadataName("Dapr.Actors.Runtime.Actor"); + var actorClass = context.SemanticModel.Compilation.GetTypeByMetadataName(DaprActorType); return classSymbol.BaseType != null && classSymbol.BaseType.Equals(actorClass, SymbolEqualityComparer.Default) ? classSymbol : null; } - private static void Execute(ImmutableArray actorTypes, + private static void Execute(Compilation compilation, ImmutableArray actorTypes, SourceProductionContext context) { var validActorTypes = actorTypes.Where(static t => t is not null).Cast().ToList(); - var source = GenerateActorRegistrationSource(validActorTypes); + var source = GenerateActorRegistrationSource(compilation, validActorTypes); context.AddSource("ActorRegistrationExtensions.g.cs", SourceText.From(source, Encoding.UTF8)); } /// /// Generates the source code for the actor registration method. /// + /// The current compilation context. /// The list of actor types to register. /// The generated source code as a string. - private static string GenerateActorRegistrationSource(IReadOnlyList actorTypes) + private static string GenerateActorRegistrationSource(Compilation compilation, IReadOnlyList actorTypes) { #pragma warning disable RS1035 var registrations = string.Join(Environment.NewLine, @@ -89,13 +95,44 @@ public static class ActorRegistrationExtensions /// /// Registers all discovered actor types with the Dapr actor runtime. /// - public static void RegisterAllActors(this IServiceCollection services) + /// The service collection to add the actors to. + /// Whether to include actor types from referenced assemblies. + public static void RegisterAllActors(this IServiceCollection services, bool includeTransientReferences = false) {{ services.AddActors(options => {{ {registrations} + if (includeTransientReferences) + {{ + {GenerateTransientActorRegistrations(compilation)} + }} }}); }} }}"; } + + /// + /// Generates the registration code for actor types in referenced assemblies. + /// + /// The current compilation context. + /// The generated registration code as a string. + private static string GenerateTransientActorRegistrations(Compilation compilation) + { + var actorRegistrations = new List(); + + foreach (var reference in compilation.References) + { + if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol referencedCompilation) + { + actorRegistrations.AddRange(from type in referencedCompilation.GlobalNamespace.GetNamespaceTypes() + where type.BaseType?.ToDisplayString() == DaprActorType + select $"options.Actors.RegisterActor<{type.ToDisplayString()}>();"); + } + } + +#pragma warning disable RS1035 + return string.Join(Environment.NewLine, actorRegistrations); +#pragma warning restore RS1035 + } } + diff --git a/src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs b/src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs index 6b45e86f3..7ea5c65f9 100644 --- a/src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs +++ b/src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs @@ -1,34 +1,33 @@ -namespace Dapr.Actors.Generators.Extensions +namespace Dapr.Actors.Generators.Extensions; + +internal static class IEnumerableExtensions { - internal static class IEnumerableExtensions + /// + /// Returns the index of the first item in the sequence that satisfies the predicate. If no item satisfies the predicate, -1 is returned. + /// + /// The type of objects in the . + /// in which to search. + /// Function performed to check whether an item satisfies the condition. + /// Return the zero-based index of the first occurrence of an element that satisfies the condition, if found; otherwise, -1. + internal static int IndexOf(this IEnumerable source, Func predicate) { - /// - /// Returns the index of the first item in the sequence that satisfies the predicate. If no item satisfies the predicate, -1 is returned. - /// - /// The type of objects in the . - /// in which to search. - /// Function performed to check whether an item satisfies the condition. - /// Return the zero-based index of the first occurrence of an element that satisfies the condition, if found; otherwise, -1. - internal static int IndexOf(this IEnumerable source, Func predicate) + if (predicate is null) { - if (predicate is null) - { - throw new ArgumentNullException(nameof(predicate)); - } + throw new ArgumentNullException(nameof(predicate)); + } - int index = 0; + int index = 0; - foreach (var item in source) + foreach (var item in source) + { + if (predicate(item)) { - if (predicate(item)) - { - return index; - } - - index++; + return index; } - return -1; + index++; } + + return -1; } } diff --git a/src/Dapr.Actors.Generators/Extensions/INamespaceSymbolExtensions.cs b/src/Dapr.Actors.Generators/Extensions/INamespaceSymbolExtensions.cs new file mode 100644 index 000000000..b094ff714 --- /dev/null +++ b/src/Dapr.Actors.Generators/Extensions/INamespaceSymbolExtensions.cs @@ -0,0 +1,33 @@ +using Microsoft.CodeAnalysis; + +namespace Dapr.Actors.Generators.Extensions; + +internal static class INamespaceSymbolExtensions +{ + /// + /// Recursively gets all the types in a namespace. + /// + /// The namespace symbol to search. + /// A collection of the named type symbols. + public static IEnumerable GetNamespaceTypes(this INamespaceSymbol namespaceSymbol) + { + foreach (var member in namespaceSymbol.GetMembers()) + { + switch (member) + { + case INamespaceSymbol nestedNamespace: + { + foreach (var nestedType in nestedNamespace.GetNamespaceTypes()) + { + yield return nestedType; + } + + break; + } + case INamedTypeSymbol namedType: + yield return namedType; + break; + } + } + } +} diff --git a/test/Dapr.Actors.Generators.Test/ActorRegistrationGeneratorTests.cs b/test/Dapr.Actors.Generators.Test/ActorRegistrationGeneratorTests.cs index c77927f1b..4ee4778e5 100644 --- a/test/Dapr.Actors.Generators.Test/ActorRegistrationGeneratorTests.cs +++ b/test/Dapr.Actors.Generators.Test/ActorRegistrationGeneratorTests.cs @@ -10,7 +10,7 @@ namespace Dapr.Actors.Generators.Test; public class ActorRegistrationGeneratorTests { [Fact] - public void TestActorRegistrationGenerator() + public void TestActorRegistrationGenerator_WithoutTransientReference() { const string source = @" using Dapr.Actors.Runtime; @@ -37,11 +37,17 @@ public static class ActorRegistrationExtensions /// /// Registers all discovered actor types with the Dapr actor runtime. /// - public static void RegisterAllActors(this IServiceCollection services) + /// The service collection to add the actors to. + /// Whether to include actor types from referenced assemblies. + public static void RegisterAllActors(this IServiceCollection services, bool includeTransientReferences = false) { services.AddActors(options => { options.Actors.RegisterActor(); + if (includeTransientReferences) + { + + } }); } }"; @@ -49,8 +55,68 @@ public static void RegisterAllActors(this IServiceCollection services) var generatedCode = GetGeneratedCode(source); Assert.Equal(expectedGeneratedCode.Trim(), generatedCode.Trim()); } + + [Fact] + public void TestActorRegistrationGenerator_WithTransientReference() + { + const string source = @" +using Dapr.Actors.Runtime; + +public class MyActor : Actor, IMyActor +{ + public MyActor(ActorHost host) : base(host) { } +} + +public interface IMyActor : IActor +{ +} +"; + + const string referencedSource = @" +using Dapr.Actors.Runtime; + +public class TransientActor : Actor, ITransientActor +{ + public TransientActor(ActorHost host) : base(host) { } +} + +public interface ITransientActor : IActor +{ +} +"; + + const string expectedGeneratedCode = @" +using Microsoft.Extensions.DependencyInjection; +using Dapr.Actors.Runtime; + +/// +/// Extension methods for registering Dapr actors. +/// +public static class ActorRegistrationExtensions +{ + /// + /// Registers all discovered actor types with the Dapr actor runtime. + /// + /// The service collection to add the actors to. + /// Whether to include actor types from referenced assemblies. + public static void RegisterAllActors(this IServiceCollection services, bool includeTransientReferences = false) + { + services.AddActors(options => + { + options.Actors.RegisterActor(); + if (includeTransientReferences) + { + options.Actors.RegisterActor(); + } + }); + } +}"; - private static string GetGeneratedCode(string source) + var generatedCode = GetGeneratedCode(source, referencedSource); + Assert.Equal(expectedGeneratedCode.Trim(), generatedCode.Trim()); + } + + private static string GetGeneratedCode(string source, string? referencedSource = null) { var syntaxTree = CSharpSyntaxTree.ParseText(SourceText.From(source, Encoding.UTF8)); var references = new List @@ -66,6 +132,17 @@ private static string GetGeneratedCode(string source) references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + if (referencedSource != null) + { + var referencedSyntaxTree = CSharpSyntaxTree.ParseText(SourceText.From(referencedSource, Encoding.UTF8)); + var referencedCompilation = CSharpCompilation.Create("ReferencedCompilation", + new[] { referencedSyntaxTree }, + references, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + compilation = compilation.AddReferences(referencedCompilation.ToMetadataReference()); + } + var generator = new ActorRegistrationGenerator(); var driver = CSharpGeneratorDriver.Create(generator); driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics); diff --git a/test/Dapr.Actors.Generators.Test/Extensions/INamespaceExtensionsTests.cs b/test/Dapr.Actors.Generators.Test/Extensions/INamespaceExtensionsTests.cs new file mode 100644 index 000000000..7b1b5b7b3 --- /dev/null +++ b/test/Dapr.Actors.Generators.Test/Extensions/INamespaceExtensionsTests.cs @@ -0,0 +1,40 @@ +using Dapr.Actors.Generators.Extensions; +using Microsoft.CodeAnalysis.CSharp; + +namespace Dapr.Actors.Generators.Test.Extensions; + +public class INamespaceExtensionsTests +{ + [Fact] + public void GetNamespaceTypes_ReturnsAllTypesInNamespace() + { + // Arrange + const string source = @" +namespace TestNamespace +{ + public class ClassA { } + public class ClassB { } + + namespace NestedNamespace + { + public class ClassC { } + } +}"; + var syntaxTree = CSharpSyntaxTree.ParseText(source); + var compilation = CSharpCompilation.Create("TestCompilation", new[] { syntaxTree }); + var namespaceSymbol = compilation.GlobalNamespace.GetNamespaceMembers().FirstOrDefault(n => n.Name == "TestNamespace"); + + // Act + if (namespaceSymbol != null) + { + var types = namespaceSymbol.GetNamespaceTypes().ToList(); + + // Assert + Assert.NotNull(namespaceSymbol); + Assert.Equal(3, types.Count); + Assert.Contains(types, t => t.Name == "ClassA"); + Assert.Contains(types, t => t.Name == "ClassB"); + Assert.Contains(types, t => t.Name == "ClassC"); + } + } +} From fe090359ff7d46a76bb70cd8709f7c8461cf3c6b Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 29 Nov 2024 23:22:03 -0600 Subject: [PATCH 3/4] Added missing using statements Signed-off-by: Whit Waldo --- src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs b/src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs index 4df130882..a917d2dd3 100644 --- a/src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs +++ b/src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs @@ -86,6 +86,8 @@ private static string GenerateActorRegistrationSource(Compilation compilation, I return $@" using Microsoft.Extensions.DependencyInjection; using Dapr.Actors.Runtime; +using Dapr.Actors; +using Dapr.Actors.AspNetCore; /// /// Extension methods for registering Dapr actors. From 54ca0377e65051ce4418e03e60cfc9cc265c4b0e Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Fri, 29 Nov 2024 23:27:19 -0600 Subject: [PATCH 4/4] More missing using statements Signed-off-by: Whit Waldo --- src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs b/src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs index a917d2dd3..f4320352c 100644 --- a/src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs +++ b/src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs @@ -85,6 +85,7 @@ private static string GenerateActorRegistrationSource(Compilation compilation, I return $@" using Microsoft.Extensions.DependencyInjection; +using Dapr.Actors.AspNetCore; using Dapr.Actors.Runtime; using Dapr.Actors; using Dapr.Actors.AspNetCore;