Skip to content

Commit 1d8b4a8

Browse files
Support tagged registrations of handlers (#374)
1 parent 35c4403 commit 1d8b4a8

59 files changed

Lines changed: 1596 additions & 124 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Immediate.Handlers.sln.DotSettings

Lines changed: 0 additions & 3 deletions
This file was deleted.

readme.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,16 @@ Calling this `AddXxxHandlers()` method will register all classes in the assembly
186186
In your `Program.cs`, add a call to `services.AddXxxBehaviors()`, where `Xxx` is the application identifier described above.
187187
Calling this method will register all behaviors referenced in any `[Behaviors]` attribute.
188188

189+
#### `Tags`
190+
191+
Assigns string tags to the registration. When `AddXxxHandlers` is called with tag arguments, only registrations that share at
192+
least one tag (or registrations with no tags) are included.
193+
194+
```csharp
195+
[Handler(Tags = ["worker", "background"])]
196+
public sealed class BackgroundWorker { }
197+
```
198+
189199
### Streaming Handlers
190200

191201
Immediate.Handlers supports streaming handlers that return `IAsyncEnumerable<TResponse>` for scenarios where
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
5+
namespace Immediate.Handlers;
6+
7+
internal static class TypedConstantExtensions
8+
{
9+
public static TypedConstant? GetArgumentValue(this ImmutableArray<KeyValuePair<string, TypedConstant>> arguments, string name)
10+
{
11+
foreach (var argument in arguments)
12+
{
13+
if (string.Equals(name, argument.Key, StringComparison.Ordinal))
14+
return argument.Value;
15+
}
16+
17+
return null;
18+
}
19+
20+
public static string? GetEnumArgumentValue(this ImmutableArray<KeyValuePair<string, TypedConstant>> arguments, string name) =>
21+
arguments.GetArgumentValue(name)?.GetEnumValueName();
22+
23+
extension(TypedConstant constant)
24+
{
25+
public string GetEnumValueName()
26+
{
27+
var fullName = constant.ToCSharpString();
28+
var start = fullName.LastIndexOf('.');
29+
return fullName[(start + 1)..];
30+
}
31+
32+
public string? GetStringArray()
33+
{
34+
if (constant.Kind != TypedConstantKind.Array)
35+
return null;
36+
37+
return string.Join(
38+
", ",
39+
constant.Values
40+
.Select(tc => tc.ToCSharpString())
41+
.OrderBy(x => x, StringComparer.Ordinal)
42+
);
43+
}
44+
45+
public INamedTypeSymbol? ArgumentType =>
46+
constant switch
47+
{
48+
{ Kind: TypedConstantKind.Type, Value: INamedTypeSymbol type } => type,
49+
_ => null,
50+
};
51+
}
52+
}

src/Common/Utility.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using Microsoft.CodeAnalysis;
2+
13
namespace Immediate.Handlers;
24

35
internal static class Utility
@@ -7,4 +9,7 @@ internal static class Utility
79

810
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> values)
911
where T : class => values.Where(x => x is not null)!;
12+
13+
public static IncrementalValuesProvider<T> WhereNotNull<T>(this IncrementalValuesProvider<T?> values)
14+
where T : class => values.Where(x => x is not null)!;
1015
}

src/Immediate.Handlers.Generators/ImmediateHandlersGenerator.cs

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,12 @@ public sealed partial class ImmediateHandlersGenerator : IIncrementalGenerator
1515
{
1616
public void Initialize(IncrementalGeneratorInitializationContext context)
1717
{
18-
var assemblyName = context.CompilationProvider
19-
.Select((cp, _) => cp.GetAssemblyIdentifier())
18+
var assemblyDefaults = context.CompilationProvider
19+
.Select((cp, _) => new AssemblyDefaults
20+
{
21+
AssemblyName = cp.GetAssemblyIdentifier(),
22+
LanguageVersion = (cp.SyntaxTrees.FirstOrDefault()?.Options as CSharpParseOptions)?.LanguageVersion ?? LanguageVersion.CSharp12,
23+
})
2024
.WithTrackingName("AssemblyName");
2125

2226
var @namespace = context
@@ -44,6 +48,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
4448
predicate: (node, _) => node is TypeDeclarationSyntax,
4549
TransformHandler.ParseHandler
4650
)
51+
.WhereNotNull()
52+
.Where(h => h.DisplayName is { } && !(h.OverrideBehaviors?.Any(b => b is null) ?? false))
4753
.WithTrackingName("Handlers");
4854

4955
var handlerNodes = handlers
@@ -62,11 +68,10 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
6268
);
6369

6470
var registrationNodes = handlers
65-
.Select((h, _) => (h?.DisplayName, h?.ServiceLifetime, h?.OverrideBehaviors))
6671
.Collect()
6772
.Combine(behaviors)
6873
.Combine(@namespace
69-
.Combine(assemblyName)
74+
.Combine(assemblyDefaults)
7075
)
7176
.WithTrackingName("Registrations");
7277

@@ -77,28 +82,22 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
7782
handlers: node.Left.Left,
7883
behaviors: node.Left.Right,
7984
@namespace: node.Right.Left,
80-
assemblyName: node.Right.Right
85+
assemblyDefaults: node.Right.Right
8186
)
8287
);
8388
}
8489

8590
private static void RenderServiceCollectionExtension(
8691
SourceProductionContext context,
87-
ImmutableArray<(string? DisplayName, string? ServiceLifetime, EquatableReadOnlyList<Behavior?>? Behaviors)> handlers,
92+
ImmutableArray<Handler> handlers,
8893
ImmutableArray<Behavior?> behaviors,
8994
string? @namespace,
90-
string assemblyName
95+
AssemblyDefaults assemblyDefaults
9196
)
9297
{
9398
var cancellationToken = context.CancellationToken;
9499
cancellationToken.ThrowIfCancellationRequested();
95100

96-
if (!handlers.Any())
97-
return;
98-
99-
if (handlers.Any(h => h.DisplayName is null || (h.Behaviors?.Any(b => b is null) ?? false)))
100-
return;
101-
102101
if (behaviors.Any(b => b is null))
103102
return;
104103

@@ -112,16 +111,22 @@ string assemblyName
112111
so.Import(
113112
new
114113
{
114+
assemblyDefaults.AssemblyName,
115+
assemblyDefaults.LanguageVersion,
115116
Namespace = @namespace,
116-
AssemblyName = assemblyName,
117-
Handlers = handlers.Select(x => new { x.DisplayName, x.ServiceLifetime }),
117+
Version = ThisAssembly.InformationalVersion,
118+
118119
Behaviors = behaviors
119-
.Concat(handlers.SelectMany(h => h.Behaviors ?? []))
120+
.Concat(handlers.SelectMany(h => h.OverrideBehaviors ?? []))
120121
.WhereNotNull()
121122
.Select(b => new { b.RegistrationType })
122123
.Distinct(),
123124

124-
Version = ThisAssembly.InformationalVersion,
125+
HandlersByTag = handlers
126+
.GroupBy(
127+
h => h.Tags,
128+
StringComparer.Ordinal
129+
),
125130
}
126131
);
127132

@@ -140,17 +145,14 @@ string assemblyName
140145

141146
private static void RenderHandler(
142147
SourceProductionContext context,
143-
Handler? handler,
148+
Handler handler,
144149
ImmutableArray<Behavior?> behaviors,
145150
Template template
146151
)
147152
{
148153
var cancellationToken = context.CancellationToken;
149154
cancellationToken.ThrowIfCancellationRequested();
150155

151-
if (handler == null)
152-
return;
153-
154156
var responseType = handler.ResponseType ?? new()
155157
{
156158
Name = "global::System.ValueTuple",
@@ -204,10 +206,11 @@ Template template
204206
private static List<Behavior?> BuildPipeline(
205207
GenericType requestType,
206208
GenericType responseType,
207-
IEnumerable<Behavior?> enumerable) =>
208-
[
209-
.. enumerable.Where(b => b.IsValid(requestType, responseType)),
210-
];
209+
IEnumerable<Behavior?> enumerable
210+
) =>
211+
[
212+
.. enumerable.Where(b => b.IsValid(requestType, responseType)),
213+
];
211214

212215
private sealed record RenderBehavior
213216
{

src/Immediate.Handlers.Generators/Models.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
using System.Diagnostics.CodeAnalysis;
2-
3-
// ReSharper disable UnusedAutoPropertyAccessor.Local
2+
using Microsoft.CodeAnalysis.CSharp;
43

54
namespace Immediate.Handlers.Generators;
65

6+
[ExcludeFromCodeCoverage]
7+
public sealed record AssemblyDefaults
8+
{
9+
public required string AssemblyName { get; init; }
10+
public required LanguageVersion LanguageVersion { get; init; }
11+
}
12+
713
[ExcludeFromCodeCoverage]
814
public sealed record Behavior
915
{
@@ -43,6 +49,7 @@ public sealed record Handler
4349
public required bool UseToken { get; init; }
4450

4551
public required string? ServiceLifetime { get; init; }
52+
public required string? Tags { get; init; }
4653

4754
public required GenericType RequestType { get; init; }
4855
public required GenericType? ResponseType { get; init; }

src/Immediate.Handlers.Generators/Properties/launchSettings.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
"profiles": {
33
"Normal": {
44
"commandName": "DebugRoslynComponent",
5-
"targetProject": "..\\..\\samples\\Normal\\Normal.csproj"
5+
"targetProject": "../../samples/Normal/Normal.csproj"
66
},
7-
"Benchmark.Simple": {
7+
"Immediate.Handlers.FunctionalTests": {
88
"commandName": "DebugRoslynComponent",
9-
"targetProject": "..\\..\\benchmarks\\Benchmark.Simple\\Benchmark.Simple.csproj"
9+
"targetProject": "../../tests/Immediate.Handlers.FunctionalTests/Immediate.Handlers.FunctionalTests.csproj"
1010
}
1111
}
12-
}
12+
}

src/Immediate.Handlers.Generators/Templates/ServiceCollectionExtensions.sbntxt

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,41 @@ public static class HandlerServiceCollectionExtensions
2323

2424
public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection Add{{ assembly_name }}Handlers(
2525
this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services,
26-
global::Microsoft.Extensions.DependencyInjection.ServiceLifetime lifetime = global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Scoped
26+
global::Microsoft.Extensions.DependencyInjection.ServiceLifetime lifetime = global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Scoped,
27+
params {{ if language_version <= 1200 }}string[]{{ else }}global::System.ReadOnlySpan<string>{{ end }} tags
2728
)
2829
{
29-
{{~ for h in handlers ~}}
30+
{{~ for g in handlers_by_tag ~}}
31+
{{~ if g.key != null ~}}
32+
if (tags is [] || Intersects(tags, [{{ g.key }}]))
33+
{
34+
{{~ end ~}}
35+
{{~ for h in g ~}}
3036
{{ h.display_name }}.AddHandlers(services, {{ if h.service_lifetime != null; "global::" + h.service_lifetime; else; "lifetime"; end }});
3137
{{~ end ~}}
32-
38+
{{~ if g.key != null ~}}
39+
}
40+
{{~ end ~}}
41+
42+
{{~ end ~}}
3343
return services;
3444
}
45+
46+
// old-fashioned inner-loop; should be fine since neither tags list will ever be large
47+
private static bool Intersects(
48+
global::System.ReadOnlySpan<string> first,
49+
global::System.ReadOnlySpan<string> second
50+
)
51+
{
52+
foreach (var f in first)
53+
{
54+
foreach (var s in second)
55+
{
56+
if (string.Equals(f, s, global::System.StringComparison.Ordinal))
57+
return true;
58+
}
59+
}
60+
61+
return false;
62+
}
3563
}

src/Immediate.Handlers.Generators/TransformHandler.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ CancellationToken cancellationToken
2222
var displayName = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
2323
var serviceLifetime = context.Attributes[0].GetServiceLifetime();
2424

25+
var tags = context.Attributes[0].NamedArguments.GetArgumentValue("Tags")?.GetStringArray();
26+
2527
var handleMethod = symbol.GetHandleMethod();
2628

2729
if (handleMethod
@@ -82,7 +84,9 @@ is null
8284
Parameters = parameters,
8385
IsStatic = isStatic,
8486
UseToken = useToken,
87+
8588
ServiceLifetime = serviceLifetime,
89+
Tags = tags,
8690

8791
RequestType = requestType,
8892
ResponseType = responseType,

src/Immediate.Handlers.Shared/HandlerAttribute.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,9 @@ public HandlerAttribute(ServiceLifetime serviceLifetime)
3636
/// </para>
3737
/// </summary>
3838
public ServiceLifetime? ServiceLifetime { get; }
39+
40+
/// <summary>
41+
/// An optional list of tags which can be used to filter the generated handlers to be registered at runtime.
42+
/// </summary>
43+
public string[]? Tags { get; init; }
3944
}

0 commit comments

Comments
 (0)