Skip to content

Added source generator for actor type registration #1401

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: release-1.16
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/itests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ jobs:
GOOS: linux
GOARCH: amd64
GOPROXY: https://proxy.golang.org
DAPR_CLI_VER: 1.14.0
DAPR_RUNTIME_VER: 1.14.0
DAPR_CLI_VER: 1.15.0
DAPR_RUNTIME_VER: 1.15.3
DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/release-1.14/install/install.sh
DAPR_CLI_REF: ''
steps:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ To load secrets into configuration call the _AddDaprSecretStore_ extension metho
Use Dapr to run the application:

```shell
dapr run --app-id SecretStoreConfigurationProviderSample --components-path ./components/ -- dotnet run
dapr run --app-id SecretStoreConfigurationProviderSample --resources-path ./components/ -- dotnet run
```

### 2. Test the application
Expand Down
2 changes: 1 addition & 1 deletion examples/Client/ConfigurationApi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ cd examples/Client/ConfigurationApi
To run the `ConfigurationExample`, execute the following command:

```bash
dapr run --app-id configexample --components-path ./Components -- dotnet run
dapr run --app-id configexample --resources-path ./Components -- dotnet run
```

### Get Configuration
Expand Down
6 changes: 3 additions & 3 deletions examples/Client/DistributedLock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ cd examples/Client/DistributedLock
In order to run the application that generates data for the workers to process, simply run the following command:

```bash
dapr run --components-path ./Components --app-id generator -- dotnet run
dapr run --resources-path ./Components --app-id generator -- dotnet run
```

This application will create a new file to process once every 10 seconds. The files are stored in `DistributedLock/tmp`.
Expand All @@ -33,8 +33,8 @@ This application will create a new file to process once every 10 seconds. The fi
In order to properly demonstrate locking, this application will be run more than once with the same App ID. However, the applications do need different ports in order to properly receive bindings. Run them with the command below:

```bash
dapr run --components-path ./Components --app-id worker --app-port 5000 -- dotnet run
dapr run --components-path ./Components --app-id worker --app-port 5001 -- dotnet run
dapr run --resources-path ./Components --app-id worker --app-port 5000 -- dotnet run
dapr run --resources-path ./Components --app-id worker --app-port 5001 -- dotnet run
```

After running the applications, they will attempt to process files. You should see output such as:
Expand Down
141 changes: 141 additions & 0 deletions src/Dapr.Actors.Generators/ActorRegistrationGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// ------------------------------------------------------------------------
// 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 Dapr.Actors.Generators.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;



namespace Dapr.Actors.Generators;

/// <summary>
/// Generates an extension method that can be used during dependency injection to register all actor types.
/// </summary>
[Generator]
public sealed class ActorRegistrationGenerator : IIncrementalGenerator
{
private const string DaprActorType = "Dapr.Actors.Runtime.Actor";

/// <summary>
/// Initializes the generator and registers the syntax receiver.
/// </summary>
/// <param name="context">The <see cref="T:Microsoft.CodeAnalysis.IncrementalGeneratorInitializationContext" /> to register callbacks on</param>
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.Left, 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(DaprActorType);
return classSymbol.BaseType != null && classSymbol.BaseType.Equals(actorClass, SymbolEqualityComparer.Default) ? classSymbol : null;
}

private static void Execute(Compilation compilation, ImmutableArray<INamedTypeSymbol?> actorTypes,
SourceProductionContext context)
{
var validActorTypes = actorTypes.Where(static t => t is not null).Cast<INamedTypeSymbol>().ToList();
var source = GenerateActorRegistrationSource(compilation, validActorTypes);
context.AddSource("ActorRegistrationExtensions.g.cs", SourceText.From(source, Encoding.UTF8));
}

/// <summary>
/// Generates the source code for the actor registration method.
/// </summary>
/// <param name="compilation">The current compilation context.</param>
/// <param name="actorTypes">The list of actor types to register.</param>
/// <returns>The generated source code as a string.</returns>
private static string GenerateActorRegistrationSource(Compilation compilation, IReadOnlyList<INamedTypeSymbol> 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.AspNetCore;
using Dapr.Actors.Runtime;
using Dapr.Actors;
using Dapr.Actors.AspNetCore;

/// <summary>
/// Extension methods for registering Dapr actors.
/// </summary>
public static class ActorRegistrationExtensions
{{
/// <summary>
/// Registers all discovered actor types with the Dapr actor runtime.
/// </summary>
/// <param name=""services"">The service collection to add the actors to.</param>
/// <param name=""includeTransientReferences"">Whether to include actor types from referenced assemblies.</param>
public static void RegisterAllActors(this IServiceCollection services, bool includeTransientReferences = false)
{{
services.AddActors(options =>
{{
{registrations}
if (includeTransientReferences)
{{
{GenerateTransientActorRegistrations(compilation)}
}}
}});
}}
}}";
}

/// <summary>
/// Generates the registration code for actor types in referenced assemblies.
/// </summary>
/// <param name="compilation">The current compilation context.</param>
/// <returns>The generated registration code as a string.</returns>
private static string GenerateTransientActorRegistrations(Compilation compilation)
{
var actorRegistrations = new List<string>();

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
}
}

45 changes: 22 additions & 23 deletions src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
namespace Dapr.Actors.Generators.Extensions
namespace Dapr.Actors.Generators.Extensions;

internal static class IEnumerableExtensions
{
internal static class IEnumerableExtensions
/// <summary>
/// Returns the index of the first item in the sequence that satisfies the predicate. If no item satisfies the predicate, -1 is returned.
/// </summary>
/// <typeparam name="T">The type of objects in the <see cref="IEnumerable{T}"/>.</typeparam>
/// <param name="source"><see cref="IEnumerable{T}"/> in which to search.</param>
/// <param name="predicate">Function performed to check whether an item satisfies the condition.</param>
/// <returns>Return the zero-based index of the first occurrence of an element that satisfies the condition, if found; otherwise, -1.</returns>
internal static int IndexOf<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
/// <summary>
/// Returns the index of the first item in the sequence that satisfies the predicate. If no item satisfies the predicate, -1 is returned.
/// </summary>
/// <typeparam name="T">The type of objects in the <see cref="IEnumerable{T}"/>.</typeparam>
/// <param name="source"><see cref="IEnumerable{T}"/> in which to search.</param>
/// <param name="predicate">Function performed to check whether an item satisfies the condition.</param>
/// <returns>Return the zero-based index of the first occurrence of an element that satisfies the condition, if found; otherwise, -1.</returns>
internal static int IndexOf<T>(this IEnumerable<T> source, Func<T, bool> 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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.CodeAnalysis;

namespace Dapr.Actors.Generators.Extensions;

internal static class INamespaceSymbolExtensions
{
/// <summary>
/// Recursively gets all the types in a namespace.
/// </summary>
/// <param name="namespaceSymbol">The namespace symbol to search.</param>
/// <returns>A collection of the named type symbols.</returns>
public static IEnumerable<INamedTypeSymbol> 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;
}
}
}
}
Loading
Loading