Skip to content

Commit c8f4681

Browse files
authored
feat: allow order enricher (#423)
* feat: add enrich after attribute * test: add enrichAfter tests * feat: compile enrich plan on start
1 parent 0155791 commit c8f4681

16 files changed

Lines changed: 489 additions & 182 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
2+
3+
/// <summary>
4+
/// Declares that the enricher must run after the specified enricher(s).
5+
/// Only references enrichers within the same <see cref="IEnricher{T}" /> group.
6+
/// </summary>
7+
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
8+
public sealed class EnrichAfterAttribute : Attribute
9+
{
10+
/// <summary>
11+
/// The enricher types that must complete before this enricher runs.
12+
/// </summary>
13+
public Type[] DependencyTypes { get; }
14+
15+
/// <summary>
16+
/// Creates a new <see cref="EnrichAfterAttribute" /> with the specified dependency enricher types.
17+
/// </summary>
18+
/// <param name="dependencyTypes">The enricher types that must complete before this enricher runs.</param>
19+
public EnrichAfterAttribute(params Type[] dependencyTypes)
20+
{
21+
ArgumentNullException.ThrowIfNull(dependencyTypes);
22+
DependencyTypes = dependencyTypes;
23+
}
24+
}

src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/EnricherBehavior.cs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections;
22
using System.Diagnostics.CodeAnalysis;
3+
using Cnblogs.Architecture.Ddd.Cqrs.Abstractions.Internals;
34
using MediatR;
45

56
namespace Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
@@ -48,23 +49,40 @@ public class EnricherBehavior<TRequest, TResponse>(IServiceProvider sp, Enricher
4849
return response;
4950
}
5051

51-
var typeInfo = cache.GetEnricherTypeInfo(elementType);
52-
if (sp.GetService(typeInfo.EnrichersServiceType) is not IEnumerable<object> enricherObjects)
52+
var enricherPlan = cache.GetEnricherPlan(elementType);
53+
if (enricherPlan is null)
5354
{
5455
return response;
5556
}
5657

57-
var enrichers = enricherObjects
58-
.Select(x => (IEnricher)x)
59-
.OrderByDescending(x => x.AllowParallel)
60-
.ToList();
58+
foreach (var stage in enricherPlan)
59+
{
60+
await RunEnrichStageAsync(stage, isEnumerable, toEnrich, cancellationToken);
61+
}
6162

62-
var method = isEnumerable ? typeInfo.BulkEnrichMethod : typeInfo.EnrichMethod;
63+
return response;
64+
}
65+
66+
private async Task RunEnrichStageAsync(
67+
EnricherStage stage,
68+
bool isEnumerable,
69+
object toEnrich,
70+
CancellationToken cancellationToken)
71+
{
6372
var parallelTasks = new List<Task>();
64-
foreach (var enricherObj in enrichers)
73+
74+
foreach (var descriptor in stage.Entries)
6575
{
76+
var enricherObj = sp.GetService(descriptor.ImplType);
77+
if (enricherObj is null)
78+
{
79+
continue;
80+
}
81+
82+
var method = isEnumerable ? descriptor.BulkEnrichMethod : descriptor.EnrichMethod;
6683
var task = (Task)method.Invoke(enricherObj, [toEnrich, cancellationToken])!;
67-
if (enricherObj.AllowParallel)
84+
85+
if (((IEnricher)enricherObj).AllowParallel)
6886
{
6987
parallelTasks.Add(task);
7088
}
@@ -75,7 +93,6 @@ public class EnricherBehavior<TRequest, TResponse>(IServiceProvider sp, Enricher
7593
}
7694

7795
await Task.WhenAll(parallelTasks);
78-
return response;
7996
}
8097

8198
private static object? UnwrapObjectResponse(object response)
@@ -153,4 +170,4 @@ private static IEnumerable Flatten(object items, ContainerInfo containerInfo)
153170

154171
return flattened;
155172
}
156-
}
173+
}

src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/EnricherMappingCache.cs

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Collections;
22
using System.Collections.Concurrent;
3+
using System.Reflection;
4+
using Cnblogs.Architecture.Ddd.Cqrs.Abstractions.Internals;
35
using Cnblogs.Architecture.Ddd.Infrastructure.Abstractions;
46

57
namespace Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
@@ -10,7 +12,7 @@ namespace Cnblogs.Architecture.Ddd.Cqrs.Abstractions;
1012
public class EnricherMappingCache
1113
{
1214
private readonly ConcurrentDictionary<Type, ContainerInfo> _containerInfoCache = new();
13-
private readonly ConcurrentDictionary<Type, EnricherTypeInfo> _enricherTypeInfoCache = new();
15+
private readonly ConcurrentDictionary<Type, List<EnricherStage>> _enricherPlans = new();
1416

1517
/// <summary>
1618
/// Get container info for a type, caching the result.
@@ -22,12 +24,104 @@ public ContainerInfo GetContainerInfo(Type type)
2224
}
2325

2426
/// <summary>
25-
/// Get enricher type info for an element type, caching the result.
27+
/// Build enrich plan with given <paramref name="targetType"/> and <paramref name="enricherTypes"/>
2628
/// </summary>
27-
/// <param name="elementType">The element type to resolve enrichers for.</param>
28-
public EnricherTypeInfo GetEnricherTypeInfo(Type elementType)
29+
/// <param name="targetType">The element type to enrich.</param>
30+
/// <param name="enricherTypes">Types of enrichers.</param>
31+
public void BuildEnrichPlan(Type targetType, ICollection<Type> enricherTypes)
2932
{
30-
return _enricherTypeInfoCache.GetOrAdd(elementType, static t => new EnricherTypeInfo(t));
33+
// ReSharper disable once HeapView.CanAvoidClosure
34+
_enricherPlans.GetOrAdd(targetType, _ => CompileEnricherPlan(enricherTypes));
35+
}
36+
37+
internal List<EnricherStage>? GetEnricherPlan(Type elementType)
38+
{
39+
var success = _enricherPlans.TryGetValue(elementType, out var list);
40+
return success ? list : null;
41+
}
42+
43+
private static List<EnricherStage> CompileEnricherPlan(ICollection<Type> enricherTypes)
44+
{
45+
if (enricherTypes.Count == 0)
46+
{
47+
return [];
48+
}
49+
50+
var descriptors = enricherTypes.ToDictionary(t => t, CreateDescriptor);
51+
var typeSet = enricherTypes.ToHashSet();
52+
var inDegree = enricherTypes.ToDictionary(x => x, _ => 0);
53+
var adjacencyList = enricherTypes.ToDictionary(x => x, _ => new List<Type>());
54+
55+
// Resolve the enricher interface for type checking
56+
var elementType = descriptors.Values.First().EnrichMethod.GetParameters()[0].ParameterType;
57+
var enricherType = typeof(IEnricher<>).MakeGenericType(elementType);
58+
59+
// Build edges from EnrichAfterAttribute
60+
foreach (var type in enricherTypes)
61+
{
62+
var attrs = type.GetCustomAttributes<EnrichAfterAttribute>();
63+
foreach (var dep in attrs.SelectMany(x => x.DependencyTypes).Where(x => x.IsAssignableTo(enricherType)))
64+
{
65+
if (typeSet.Contains(dep))
66+
{
67+
// Edge: dep -> type (dep must run BEFORE type)
68+
adjacencyList[dep].Add(type);
69+
inDegree[type]++;
70+
continue;
71+
}
72+
73+
// Dependency is not in the same group, this is a configuration error
74+
throw new InvalidOperationException(
75+
$"Enricher '{type}' depends on '{dep}', but '{dep}' is not registered/resolved for this element type.");
76+
}
77+
}
78+
79+
return BuildDag(enricherTypes, descriptors, inDegree, adjacencyList);
80+
}
81+
82+
private static EnricherDescriptor CreateDescriptor(Type enricherType)
83+
{
84+
var enricherInterface = enricherType.GetInterfaces()
85+
.First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnricher<>));
86+
return new EnricherDescriptor(
87+
enricherType,
88+
enricherInterface.GetMethod("EnrichAsync")!,
89+
enricherInterface.GetMethod("BulkEnrichAsync")!);
90+
}
91+
92+
private static List<EnricherStage> BuildDag(
93+
ICollection<Type> enricherTypes,
94+
Dictionary<Type, EnricherDescriptor> descriptors,
95+
Dictionary<Type, int> inDegree,
96+
Dictionary<Type, List<Type>> adjacencyList)
97+
{
98+
var stages = new List<EnricherStage>();
99+
var currentStageNodes = inDegree.Where(x => x.Value == 0).Select(x => x.Key).ToList();
100+
var processedCount = 0;
101+
102+
// Process level by level
103+
while (currentStageNodes.Count > 0)
104+
{
105+
stages.Add(new EnricherStage(currentStageNodes.Select(t => descriptors[t]).ToList()));
106+
processedCount += currentStageNodes.Count;
107+
108+
var nextStageNodes = new List<Type>();
109+
foreach (var dependent in currentStageNodes.SelectMany(node => adjacencyList[node]))
110+
{
111+
inDegree[dependent]--;
112+
if (inDegree[dependent] == 0)
113+
{
114+
nextStageNodes.Add(dependent);
115+
}
116+
}
117+
118+
currentStageNodes = nextStageNodes;
119+
}
120+
121+
// Circular dependency check
122+
return processedCount == enricherTypes.Count
123+
? stages
124+
: throw new InvalidOperationException("Circular dependency detected among enrichers.");
31125
}
32126

33127
private static ContainerInfo ResolveContainerInfo(Type t)

src/Cnblogs.Architecture.Ddd.Cqrs.Abstractions/EnricherTypeInfo.cs

Lines changed: 0 additions & 41 deletions
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
using System.Reflection;
2+
3+
namespace Cnblogs.Architecture.Ddd.Cqrs.Abstractions.Internals;
4+
5+
internal record EnricherDescriptor(Type ImplType, MethodInfo EnrichMethod, MethodInfo BulkEnrichMethod);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace Cnblogs.Architecture.Ddd.Cqrs.Abstractions.Internals;
2+
3+
internal record EnricherStage(List<EnricherDescriptor> Entries);

0 commit comments

Comments
 (0)