Skip to content
Merged
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
25 changes: 22 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,19 @@ public sealed class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior
}
```

This can be registered assembly-wide using:
### Using Behaviors

Once added to the pipeline, the behavior will be called as part of the pipeline to handle a request. They can be added
to the pipeline one of three ways:

* Behaviors can be registered assembly-wide by using an `[assembly: ]` attribute, as shown here:
```cs
[assembly: Behaviors(
typeof(LoggingBehavior<,>)
)]
```

or on an individual handler using:
* Behaviors can be applied on an individual handler using:
```cs
[Handler]
[Behavior(
Expand All @@ -134,7 +139,21 @@ public static class GetUsersQuery
}
```

Once added to the pipeline, the behavior will be called as part of the pipeline to handle a request.
* Common behavior pipelines can be defined by applying a `[Behaviors]` attribute another attribute, as shown here:
```cs
[Behaviors(
typeof(ValidationBehavior<,>), typeof(TransactionBehavior<,>)
)]
public sealed class DefaultBehaviorsAttribute : Attribute;

// usage
[Handler]
[DefaultBehaviors]
public static class GetUsersQuery
{
// ..
}
```

Note: adding a `[Behavior]` attribute to a handler will disregard all assembly-wide behaviors for that handler, so any
global behaviors necessary must be independently added to the handler override behaviors list.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,25 @@ private static void AddBaseTypes(ITypeSymbol type, List<string> implements)

file static class Extensions
{
public static AttributeData? GetBehaviorsAttribute(this INamedTypeSymbol symbol) =>
symbol
.GetAttributes()
.FirstOrDefault(a => a.AttributeClass.IsBehaviorsAttribute());
public static AttributeData? GetBehaviorsAttribute(this INamedTypeSymbol symbol)
{
foreach (var a in symbol.GetAttributes())
{
if (a.AttributeClass is null)
continue;

if (a.AttributeClass.IsBehaviorsAttribute())
return a;

foreach (var aa in a.AttributeClass.GetAttributes())
{
if (aa.AttributeClass.IsBehaviorsAttribute())
return aa;
}
}

return null;
}

public static string? GetAttributesString(this IParameterSymbol parameter)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//HintName: IH.Dummy.GetUsersQuery.g.cs
using Microsoft.Extensions.DependencyInjection;

#pragma warning disable CS1591

namespace Dummy;

partial class GetUsersQuery
{
public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>>
{
private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior;
private readonly global::Dummy.LoggingBehavior<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>> _loggingBehavior;

public Handler(
global::Dummy.GetUsersQuery.HandleBehavior handleBehavior,
global::Dummy.LoggingBehavior<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>> loggingBehavior
)
{
var handlerType = typeof(GetUsersQuery);

_handleBehavior = handleBehavior;

_loggingBehavior = loggingBehavior;
_loggingBehavior.HandlerType = handlerType;

_loggingBehavior.SetInnerHandler(_handleBehavior);
}

public async global::System.Threading.Tasks.ValueTask<global::System.Collections.Generic.IEnumerable<global::Dummy.User>> HandleAsync(
global::Dummy.GetUsersQuery.Query request,
global::System.Threading.CancellationToken cancellationToken = default
)
{
return await _loggingBehavior
.HandleAsync(request, cancellationToken)
.ConfigureAwait(false);
}
}

[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>>
{
private readonly global::Dummy.UsersService _usersService;

public HandleBehavior(
global::Dummy.UsersService usersService
)
{
_usersService = usersService;
}

public override async global::System.Threading.Tasks.ValueTask<global::System.Collections.Generic.IEnumerable<global::Dummy.User>> HandleAsync(
global::Dummy.GetUsersQuery.Query request,
global::System.Threading.CancellationToken cancellationToken
)
{
return await global::Dummy.GetUsersQuery
.HandleAsync(
request
, _usersService
, cancellationToken
)
.ConfigureAwait(false);
}
}

[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public static IServiceCollection AddHandlers(
IServiceCollection services,
ServiceLifetime lifetime = ServiceLifetime.Scoped
)
{
services.Add(new(typeof(global::Dummy.GetUsersQuery.Handler), typeof(global::Dummy.GetUsersQuery.Handler), lifetime));
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));
services.Add(new(typeof(global::Dummy.GetUsersQuery.HandleBehavior), typeof(global::Dummy.GetUsersQuery.HandleBehavior), lifetime));
return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//HintName: IH.ServiceCollectionExtensions.g.cs
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

#pragma warning disable CS1591

public static class HandlerServiceCollectionExtensions
{
public static IServiceCollection AddTestsBehaviors(
this IServiceCollection services)
{
services.TryAddTransient(typeof(global::Dummy.LoggingBehavior<,>));

return services;
}

public static IServiceCollection AddTestsHandlers(
this IServiceCollection services,
ServiceLifetime lifetime = ServiceLifetime.Scoped
)
{
global::Dummy.GetUsersQuery.AddHandlers(services, lifetime);

return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//HintName: IH.Dummy.GetUsersQuery.g.cs
#pragma warning disable CS1591

namespace Dummy;

partial class GetUsersQuery
{
public sealed partial class Handler : global::Immediate.Handlers.Shared.IHandler<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>>
{
private readonly global::Dummy.GetUsersQuery.HandleBehavior _handleBehavior;
private readonly global::Dummy.LoggingBehavior<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>> _loggingBehavior;

public Handler(
global::Dummy.GetUsersQuery.HandleBehavior handleBehavior,
global::Dummy.LoggingBehavior<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>> loggingBehavior
)
{
var handlerType = typeof(GetUsersQuery);

_handleBehavior = handleBehavior;

_loggingBehavior = loggingBehavior;
_loggingBehavior.HandlerType = handlerType;

_loggingBehavior.SetInnerHandler(_handleBehavior);
}

public async global::System.Threading.Tasks.ValueTask<global::System.Collections.Generic.IEnumerable<global::Dummy.User>> HandleAsync(
global::Dummy.GetUsersQuery.Query request,
global::System.Threading.CancellationToken cancellationToken = default
)
{
return await _loggingBehavior
.HandleAsync(request, cancellationToken)
.ConfigureAwait(false);
}
}

[global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
public sealed class HandleBehavior : global::Immediate.Handlers.Shared.Behavior<global::Dummy.GetUsersQuery.Query, global::System.Collections.Generic.IEnumerable<global::Dummy.User>>
{
private readonly global::Dummy.UsersService _usersService;

public HandleBehavior(
global::Dummy.UsersService usersService
)
{
_usersService = usersService;
}

public override async global::System.Threading.Tasks.ValueTask<global::System.Collections.Generic.IEnumerable<global::Dummy.User>> HandleAsync(
global::Dummy.GetUsersQuery.Query request,
global::System.Threading.CancellationToken cancellationToken
)
{
return await global::Dummy.GetUsersQuery
.HandleAsync(
request
, _usersService
, cancellationToken
)
.ConfigureAwait(false);
}
}
}
88 changes: 88 additions & 0 deletions tests/Immediate.Handlers.Tests/GeneratorTests/BehaviorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,92 @@ CancellationToken __
_ = await Verify(result)
.UseParameters(string.Join('_', assemblies));
}

[Test]
[Arguments(DriverReferenceAssemblies.Normal)]
[Arguments(DriverReferenceAssemblies.Msdi)]
public async Task NestedBehavior(DriverReferenceAssemblies assemblies)
{
var result = GeneratorTestHelper.RunGenerator(
"""
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Dummy;
using Immediate.Handlers.Shared;

#pragma warning disable CS9113

namespace Dummy;

[Behaviors(
typeof(LoggingBehavior<,>)
)]
[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class)]
public sealed class DefaultBehaviorsAttribute : Attribute;

public class GetUsersEndpoint(GetUsersQuery.Handler handler)
{
public ValueTask<IEnumerable<User>> GetUsers() =>
handler.HandleAsync(new GetUsersQuery.Query());
}

[Handler]
[DefaultBehaviors]
public static partial class GetUsersQuery
{
public record Query;

private static ValueTask<IEnumerable<User>> HandleAsync(
Query _,
UsersService usersService,
CancellationToken token)
{
return usersService.GetUsers();
}
}

public class LoggingBehavior<TRequest, TResponse>(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
: Behavior<TRequest, TResponse>
{
public override async ValueTask<TResponse> HandleAsync(TRequest request, CancellationToken cancellationToken)
{
var response = await Next(request, cancellationToken);

return response;
}
}

public class User { }
public class UsersService
{
public ValueTask<IEnumerable<User>> GetUsers() =>
ValueTask.FromResult(Enumerable.Empty<User>());
}

public interface ILogger<T>;
""",
assemblies
);

Assert.Equal(
[
"Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlers.ImmediateHandlersGenerator/IH.Dummy.GetUsersQuery.g.cs",
.. assemblies switch
{
DriverReferenceAssemblies.Normal => Enumerable.Empty<string>(),
DriverReferenceAssemblies.Msdi =>
["Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlers.ImmediateHandlersGenerator/IH.ServiceCollectionExtensions.g.cs"],

DriverReferenceAssemblies.None or _ => throw new UnreachableException(),
},
],
result.GeneratedTrees.Select(t => t.FilePath.Replace('\\', '/'))
);

_ = await Verify(result)
.UseParameters(string.Join('_', assemblies));
}
}