Skip to content

Commit 58a537c

Browse files
Support shared Behaviors list (#211)
1 parent b953790 commit 58a537c

6 files changed

+299
-7
lines changed

readme.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,14 +115,19 @@ public sealed class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior
115115
}
116116
```
117117

118-
This can be registered assembly-wide using:
118+
### Using Behaviors
119+
120+
Once added to the pipeline, the behavior will be called as part of the pipeline to handle a request. They can be added
121+
to the pipeline one of three ways:
122+
123+
* Behaviors can be registered assembly-wide by using an `[assembly: ]` attribute, as shown here:
119124
```cs
120125
[assembly: Behaviors(
121126
typeof(LoggingBehavior<,>)
122127
)]
123128
```
124129

125-
or on an individual handler using:
130+
* Behaviors can be applied on an individual handler using:
126131
```cs
127132
[Handler]
128133
[Behavior(
@@ -134,7 +139,21 @@ public static class GetUsersQuery
134139
}
135140
```
136141

137-
Once added to the pipeline, the behavior will be called as part of the pipeline to handle a request.
142+
* Common behavior pipelines can be defined by applying a `[Behaviors]` attribute another attribute, as shown here:
143+
```cs
144+
[Behaviors(
145+
typeof(ValidationBehavior<,>), typeof(TransactionBehavior<,>)
146+
)]
147+
public sealed class DefaultBehaviorsAttribute : Attribute;
148+
149+
// usage
150+
[Handler]
151+
[DefaultBehaviors]
152+
public static class GetUsersQuery
153+
{
154+
// ..
155+
}
156+
```
138157

139158
Note: adding a `[Behavior]` attribute to a handler will disregard all assembly-wide behaviors for that handler, so any
140159
global behaviors necessary must be independently added to the handler override behaviors list.

src/Immediate.Handlers.Generators/ImmediateHandlersGenerator.TransformHandler.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,25 @@ private static void AddBaseTypes(ITypeSymbol type, List<string> implements)
134134

135135
file static class Extensions
136136
{
137-
public static AttributeData? GetBehaviorsAttribute(this INamedTypeSymbol symbol) =>
138-
symbol
139-
.GetAttributes()
140-
.FirstOrDefault(a => a.AttributeClass.IsBehaviorsAttribute());
137+
public static AttributeData? GetBehaviorsAttribute(this INamedTypeSymbol symbol)
138+
{
139+
foreach (var a in symbol.GetAttributes())
140+
{
141+
if (a.AttributeClass is null)
142+
continue;
143+
144+
if (a.AttributeClass.IsBehaviorsAttribute())
145+
return a;
146+
147+
foreach (var aa in a.AttributeClass.GetAttributes())
148+
{
149+
if (aa.AttributeClass.IsBehaviorsAttribute())
150+
return aa;
151+
}
152+
}
153+
154+
return null;
155+
}
141156

142157
public static string? GetAttributesString(this IParameterSymbol parameter)
143158
{
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
//HintName: IH.Dummy.GetUsersQuery.g.cs
2+
using Microsoft.Extensions.DependencyInjection;
3+
4+
#pragma warning disable CS1591
5+
6+
namespace Dummy;
7+
8+
partial class GetUsersQuery
9+
{
10+
public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>>
11+
{
12+
private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior;
13+
private readonly global::Dummy.LoggingBehavior<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>> _loggingBehavior;
14+
15+
public Handler(
16+
global::Dummy.GetUsersQuery.HandleBehavior handleBehavior,
17+
global::Dummy.LoggingBehavior<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>> loggingBehavior
18+
)
19+
{
20+
var handlerType = typeof(GetUsersQuery);
21+
22+
_handleBehavior = handleBehavior;
23+
24+
_loggingBehavior = loggingBehavior;
25+
_loggingBehavior.HandlerType = handlerType;
26+
27+
_loggingBehavior.SetInnerHandler(_handleBehavior);
28+
}
29+
30+
public async global::System.Threading.Tasks.ValueTask<global::System.Collections.Generic.IEnumerable<global::Dummy.User>> HandleAsync(
31+
global::Dummy.GetUsersQuery.Query request,
32+
global::System.Threading.CancellationToken cancellationToken = default
33+
)
34+
{
35+
return await _loggingBehavior
36+
.HandleAsync(request, cancellationToken)
37+
.ConfigureAwait(false);
38+
}
39+
}
40+
41+
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
42+
public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>>
43+
{
44+
private readonly global::Dummy.UsersService _usersService;
45+
46+
public HandleBehavior(
47+
global::Dummy.UsersService usersService
48+
)
49+
{
50+
_usersService = usersService;
51+
}
52+
53+
public override async global::System.Threading.Tasks.ValueTask<global::System.Collections.Generic.IEnumerable<global::Dummy.User>> HandleAsync(
54+
global::Dummy.GetUsersQuery.Query request,
55+
global::System.Threading.CancellationToken cancellationToken
56+
)
57+
{
58+
return await global::Dummy.GetUsersQuery
59+
.HandleAsync(
60+
request
61+
, _usersService
62+
, cancellationToken
63+
)
64+
.ConfigureAwait(false);
65+
}
66+
}
67+
68+
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
69+
public static IServiceCollection AddHandlers(
70+
IServiceCollection services,
71+
ServiceLifetime lifetime = ServiceLifetime.Scoped
72+
)
73+
{
74+
services.Add(new(typeof(global::Dummy.GetUsersQuery.Handler), typeof(global::Dummy.GetUsersQuery.Handler), lifetime));
75+
services.Add(new(typeof(global::Immediate.Handlers.Shared.IHandler<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>>), typeof(global::Dummy.GetUsersQuery.Handler), lifetime));
76+
services.Add(new(typeof(global::Dummy.GetUsersQuery.HandleBehavior), typeof(global::Dummy.GetUsersQuery.HandleBehavior), lifetime));
77+
return services;
78+
}
79+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//HintName: IH.ServiceCollectionExtensions.g.cs
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.DependencyInjection.Extensions;
4+
5+
#pragma warning disable CS1591
6+
7+
public static class HandlerServiceCollectionExtensions
8+
{
9+
public static IServiceCollection AddTestsBehaviors(
10+
this IServiceCollection services)
11+
{
12+
services.TryAddTransient(typeof(global::Dummy.LoggingBehavior<,>));
13+
14+
return services;
15+
}
16+
17+
public static IServiceCollection AddTestsHandlers(
18+
this IServiceCollection services,
19+
ServiceLifetime lifetime = ServiceLifetime.Scoped
20+
)
21+
{
22+
global::Dummy.GetUsersQuery.AddHandlers(services, lifetime);
23+
24+
return services;
25+
}
26+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//HintName: IH.Dummy.GetUsersQuery.g.cs
2+
#pragma warning disable CS1591
3+
4+
namespace Dummy;
5+
6+
partial class GetUsersQuery
7+
{
8+
public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>>
9+
{
10+
private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior;
11+
private readonly global::Dummy.LoggingBehavior<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>> _loggingBehavior;
12+
13+
public Handler(
14+
global::Dummy.GetUsersQuery.HandleBehavior handleBehavior,
15+
global::Dummy.LoggingBehavior<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>> loggingBehavior
16+
)
17+
{
18+
var handlerType = typeof(GetUsersQuery);
19+
20+
_handleBehavior = handleBehavior;
21+
22+
_loggingBehavior = loggingBehavior;
23+
_loggingBehavior.HandlerType = handlerType;
24+
25+
_loggingBehavior.SetInnerHandler(_handleBehavior);
26+
}
27+
28+
public async global::System.Threading.Tasks.ValueTask<global::System.Collections.Generic.IEnumerable<global::Dummy.User>> HandleAsync(
29+
global::Dummy.GetUsersQuery.Query request,
30+
global::System.Threading.CancellationToken cancellationToken = default
31+
)
32+
{
33+
return await _loggingBehavior
34+
.HandleAsync(request, cancellationToken)
35+
.ConfigureAwait(false);
36+
}
37+
}
38+
39+
[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
40+
public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>>
41+
{
42+
private readonly global::Dummy.UsersService _usersService;
43+
44+
public HandleBehavior(
45+
global::Dummy.UsersService usersService
46+
)
47+
{
48+
_usersService = usersService;
49+
}
50+
51+
public override async global::System.Threading.Tasks.ValueTask<global::System.Collections.Generic.IEnumerable<global::Dummy.User>> HandleAsync(
52+
global::Dummy.GetUsersQuery.Query request,
53+
global::System.Threading.CancellationToken cancellationToken
54+
)
55+
{
56+
return await global::Dummy.GetUsersQuery
57+
.HandleAsync(
58+
request
59+
, _usersService
60+
, cancellationToken
61+
)
62+
.ConfigureAwait(false);
63+
}
64+
}
65+
}

tests/Immediate.Handlers.Tests/GeneratorTests/BehaviorTests.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,4 +306,92 @@ CancellationToken __
306306
_ = await Verify(result)
307307
.UseParameters(string.Join('_', assemblies));
308308
}
309+
310+
[Test]
311+
[Arguments(DriverReferenceAssemblies.Normal)]
312+
[Arguments(DriverReferenceAssemblies.Msdi)]
313+
public async Task NestedBehavior(DriverReferenceAssemblies assemblies)
314+
{
315+
var result = GeneratorTestHelper.RunGenerator(
316+
"""
317+
using System;
318+
using System.Collections.Generic;
319+
using System.Linq;
320+
using System.Threading;
321+
using System.Threading.Tasks;
322+
using Dummy;
323+
using Immediate.Handlers.Shared;
324+
325+
#pragma warning disable CS9113
326+
327+
namespace Dummy;
328+
329+
[Behaviors(
330+
typeof(LoggingBehavior<,>)
331+
)]
332+
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class)]
333+
public sealed class DefaultBehaviorsAttribute : Attribute;
334+
335+
public class GetUsersEndpoint(GetUsersQuery.Handler handler)
336+
{
337+
public ValueTask<IEnumerable<User>> GetUsers() =>
338+
handler.HandleAsync(new GetUsersQuery.Query());
339+
}
340+
341+
[Handler]
342+
[DefaultBehaviors]
343+
public static partial class GetUsersQuery
344+
{
345+
public record Query;
346+
347+
private static ValueTask<IEnumerable<User>> HandleAsync(
348+
Query _,
349+
UsersService usersService,
350+
CancellationToken token)
351+
{
352+
return usersService.GetUsers();
353+
}
354+
}
355+
356+
public class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
357+
: Behavior<TRequest, TResponse>
358+
{
359+
public override async ValueTask<TResponse> HandleAsync(TRequest request, CancellationToken cancellationToken)
360+
{
361+
var response = await Next(request, cancellationToken);
362+
363+
return response;
364+
}
365+
}
366+
367+
public class User { }
368+
public class UsersService
369+
{
370+
public ValueTask<IEnumerable<User>> GetUsers() =>
371+
ValueTask.FromResult(Enumerable.Empty<User>());
372+
}
373+
374+
public interface ILogger<T>;
375+
""",
376+
assemblies
377+
);
378+
379+
Assert.Equal(
380+
[
381+
"Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlers.ImmediateHandlersGenerator/IH.Dummy.GetUsersQuery.g.cs",
382+
.. assemblies switch
383+
{
384+
DriverReferenceAssemblies.Normal => Enumerable.Empty<string>(),
385+
DriverReferenceAssemblies.Msdi =>
386+
["Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlers.ImmediateHandlersGenerator/IH.ServiceCollectionExtensions.g.cs"],
387+
388+
DriverReferenceAssemblies.None or _ => throw new UnreachableException(),
389+
},
390+
],
391+
result.GeneratedTrees.Select(t => t.FilePath.Replace('\\', '/'))
392+
);
393+
394+
_ = await Verify(result)
395+
.UseParameters(string.Join('_', assemblies));
396+
}
309397
}

0 commit comments

Comments
 (0)