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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,5 @@ _NCrunch_*

## Verify snapshot tests
*.received.*

.nuke/temp/
27 changes: 16 additions & 11 deletions DesignPatterns.SourceGenerators/Generators/HandlerOrderGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,28 @@ public void Initialize(IncrementalGeneratorInitializationContext context)

context.RegisterSourceOutput(
nonGeneric.Collect().Combine(generic.Collect()).Combine(diEnabled),
static (spc, source) => Execute(spc, source.Left.Left, source.Left.Right, source.Right));
static (spc, source) => Execute(spc,
source.Left.Left.SelectMany(static list => list).ToImmutableArray(),
source.Left.Right.SelectMany(static list => list).ToImmutableArray(),
source.Right));
}

private static HandlerRegistration? Transform(GeneratorAttributeSyntaxContext context, bool isGenericAttribute)
private static List<HandlerRegistration> Transform(GeneratorAttributeSyntaxContext context, bool isGenericAttribute)
{
var result = new List<HandlerRegistration>();

if (context.TargetSymbol is not INamedTypeSymbol handlerType)
{
return null;
return result;
}

if (context.Attributes.IsDefaultOrEmpty)
{
return null;
return result;
}

var location = context.TargetNode.GetLocation();

foreach (var attribute in context.Attributes)
{
if (attribute.ConstructorArguments.Length == 0)
Expand Down Expand Up @@ -109,26 +116,24 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
continue;
}

return new HandlerRegistration(
result.Add(new HandlerRegistration(
order,
contextType,
handlerType,
context.TargetNode.GetLocation());
location));
}

return null;
return result;
}

private static void Execute(
SourceProductionContext context,
ImmutableArray<HandlerRegistration?> nonGeneric,
ImmutableArray<HandlerRegistration?> generic,
ImmutableArray<HandlerRegistration> nonGeneric,
ImmutableArray<HandlerRegistration> generic,
bool enableDiIntegration)
{
var registrations = nonGeneric
.Concat(generic)
.Where(static r => r is not null)
.Cast<HandlerRegistration>()
.ToList();

if (registrations.Count == 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace DesignPatterns.Behavioral;
/// Lower values run first.
/// </summary>
/// <typeparam name="TContext">The context type flowing through the pipeline.</typeparam>
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public sealed class HandlerOrderAttribute<TContext> : Attribute
{
/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion DesignPatterns/Behavioral/HandlerOrderAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace DesignPatterns.Behavioral;
/// Specifies the execution order of a handler within a generated pipeline for the given context type.
/// Lower values run first. Use the generic attribute when the target framework supports generic attributes (C# 11+).
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public sealed class HandlerOrderAttribute : Attribute
{
/// <summary>
Expand Down
2 changes: 2 additions & 0 deletions docs/ChainOfResponsibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public sealed class LoggingHandler : IHandler<RequestContext> { }

数值越小越先执行(与手动 `Use` 注册顺序一致)。

同一 handler 类可标注多个 `[HandlerOrder<...>]`(`AllowMultiple = true`),分别加入不同 `TContext` 的生成管道,例如共享日志 handler 同时实现 `IHandler<RequestContext>` 与 `IHandler<AuditContext>`。同一 context 下重复的 Order 仍报 **DP005**。

### 生成输出

对每种 `TContext` 生成 `{Context}HandlerPipeline`:
Expand Down
3 changes: 2 additions & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ Decorator 设计详见 [Decorator.md](Decorator.md)。

| 项 | 说明 |
|----|------|
| Handler `AllowMultiple` | 单类多 `TContext` |
| ~~Handler `AllowMultiple`~~ | 单类多 `TContext`(已完成) |
| Composite 多根/森林 | v2 设计 |
| Composite 手动 `CompositeTreeBuilder` | Sample 分支演示 |
| CompletionProvider | IDE 补全,维护成本高 |

## 已完成(工程化)

- Handler `AllowMultiple`:单类多个 `[HandlerOrder]` / `[HandlerOrder<TContext>]`;生成器按特性实例注册;集成测试
- DI 与生成器打通:`DesignPatterns_EnableDiIntegration` targets;Strategy / Factory / Handler 生成 `RegisterDi` + `Create(IServiceProvider)`;`ServiceProviderStrategyRegistry`;集成测试
- M2 EventAggregator:`IEventAggregator`、`IEventHandler<T>`、`EventAggregator` 实现、单元测试、Sample
- P3 RegisterFactory:`[RegisterFactory]` 属性 + `RegisterFactoryGenerator`、DP020–022、集成测试
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
AuditContext.AuditContextHandlerPipeline.g.cs:
using System;
using DesignPatterns.Behavioral;

namespace TestAssembly
{
public static partial class AuditContextHandlerPipeline
{
private static readonly HandlerPipeline<global::TestAssembly.AuditContext> _instance = new HandlerPipelineBuilder<global::TestAssembly.AuditContext>().Use(new global::TestAssembly.SharedLoggingHandler()).Build();
public static HandlerPipeline<global::TestAssembly.AuditContext> Instance
{
get
{
return _instance;
}
}
}
}// <auto-generated />
// Generated by DesignPatterns.SourceGenerators.HandlerOrderGenerator
,
RequestContext.RequestContextHandlerPipeline.g.cs:
using System;
using DesignPatterns.Behavioral;

namespace TestAssembly
{
public static partial class RequestContextHandlerPipeline
{
private static readonly HandlerPipeline<global::TestAssembly.RequestContext> _instance = new HandlerPipelineBuilder<global::TestAssembly.RequestContext>().Use(new global::TestAssembly.SharedLoggingHandler()).Build();
public static HandlerPipeline<global::TestAssembly.RequestContext> Instance
{
get
{
return _instance;
}
}
}
}// <auto-generated />
// Generated by DesignPatterns.SourceGenerators.HandlerOrderGenerator

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[
{
Id: DP005,
Message: Handler order '10' is already used for context 'TestAssembly.RequestContext'
},
{
Id: DP005,
Message: Handler order '10' is already used for context 'TestAssembly.RequestContext'
}
]
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,82 @@ public ValueTask InvokeAsync(
return Verifier.Verify(SourceGeneratorTestContext.GetGeneratedSources(runResult));
}

[Fact]
public Task GeneratesPipelinesWhenSingleClassHasMultipleContexts()
{
const string source = """
using System.Threading;
using System.Threading.Tasks;
using DesignPatterns.Behavioral;

namespace TestAssembly;

public sealed class RequestContext
{
}

public sealed class AuditContext
{
}

[HandlerOrder<RequestContext>(10)]
[HandlerOrder<AuditContext>(10)]
public sealed class SharedLoggingHandler :
IHandler<RequestContext>,
IHandler<AuditContext>
{
public ValueTask InvokeAsync(
RequestContext context,
HandlerDelegate<RequestContext> next,
CancellationToken cancellationToken = default) =>
next(context, cancellationToken);

public ValueTask InvokeAsync(
AuditContext context,
HandlerDelegate<AuditContext> next,
CancellationToken cancellationToken = default) =>
next(context, cancellationToken);
}
""";

var runResult = SourceGeneratorTestContext.Run<HandlerOrderGenerator>(
("Handlers.cs", source));

return Verifier.Verify(SourceGeneratorTestContext.GetGeneratedSources(runResult));
}

[Fact]
public Task ReportsDp005DuplicateOrderOnSameClass()
{
const string source = """
using System.Threading;
using System.Threading.Tasks;
using DesignPatterns.Behavioral;

namespace TestAssembly;

public sealed class RequestContext
{
}

[HandlerOrder<RequestContext>(10)]
[HandlerOrder<RequestContext>(10)]
public sealed class DuplicateOrderHandler : IHandler<RequestContext>
{
public ValueTask InvokeAsync(
RequestContext context,
HandlerDelegate<RequestContext> next,
CancellationToken cancellationToken = default) =>
default;
}
""";

var runResult = SourceGeneratorTestContext.Run<HandlerOrderGenerator>(
("Handlers.cs", source));

return Verifier.Verify(SourceGeneratorTestContext.GetGeneratorDiagnostics(runResult));
}

[Fact]
public Task ReportsDp005DuplicateOrder()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using DesignPatterns.Behavioral;

namespace DesignPatterns.Tests.Integration.Chain;

public sealed class MultiContextRequest
{
public bool Handled { get; set; }
}

public sealed class MultiContextAudit
{
public bool Handled { get; set; }
}

[HandlerOrder<MultiContextRequest>(10)]
[HandlerOrder<MultiContextAudit>(10)]
public sealed class MultiContextSharedHandler :
IHandler<MultiContextRequest>,
IHandler<MultiContextAudit>
{
public ValueTask InvokeAsync(
MultiContextRequest context,
HandlerDelegate<MultiContextRequest> next,
CancellationToken cancellationToken = default)
{
context.Handled = true;
return next(context, cancellationToken);
}

public ValueTask InvokeAsync(
MultiContextAudit context,
HandlerDelegate<MultiContextAudit> next,
CancellationToken cancellationToken = default)
{
context.Handled = true;
return next(context, cancellationToken);
}
}

public sealed class MultiContextHandlerIntegrationTests
{
[Fact]
public async Task SharedHandler_AppearsInBothGeneratedPipelines()
{
var request = new MultiContextRequest();
await MultiContextRequestHandlerPipeline.Instance.InvokeAsync(request);
Assert.True(request.Handled);

var audit = new MultiContextAudit();
await MultiContextAuditHandlerPipeline.Instance.InvokeAsync(audit);
Assert.True(audit.Handled);
}
}
Loading