Skip to content

Commit f445340

Browse files
authored
Merge pull request #18 from FoundatioFx/copilot/fix-non-public-middleware-usage
Enforce middleware accessibility rules: private rejected, internal scoped to assembly
2 parents 58153e3 + 4a6bf78 commit f445340

File tree

5 files changed

+189
-0
lines changed

5 files changed

+189
-0
lines changed

src/Foundatio.Mediator/MetadataMiddlewareScanner.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ public override void VisitNamedType(INamedTypeSymbol symbol)
7979
if (symbol.HasIgnoreAttribute(_compilation))
8080
return;
8181

82+
// Skip internal or private middleware from cross-assembly usage
83+
// Only public middleware can be used across assemblies
84+
if (symbol.DeclaredAccessibility != Accessibility.Public)
85+
return;
86+
8287
// Try to extract middleware info from metadata
8388
var middlewareInfo = ExtractMiddlewareInfo(symbol);
8489
if (middlewareInfo != null)
@@ -156,6 +161,8 @@ public override void VisitNamedType(INamedTypeSymbol symbol)
156161
BeforeMethod = beforeMethod != null ? CreateMiddlewareMethodInfo(beforeMethod) : null,
157162
AfterMethod = afterMethod != null ? CreateMiddlewareMethodInfo(afterMethod) : null,
158163
FinallyMethod = finallyMethod != null ? CreateMiddlewareMethodInfo(finallyMethod) : null,
164+
DeclaredAccessibility = classSymbol.DeclaredAccessibility,
165+
AssemblyName = classSymbol.ContainingAssembly.Name,
159166
Diagnostics = new EquatableArray<DiagnosticInfo>([]) // No diagnostics for metadata-based
160167
};
161168
}

src/Foundatio.Mediator/MiddlewareAnalyzer.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,19 @@ public static bool IsMatch(SyntaxNode node)
139139
if (messageType == null)
140140
return null;
141141

142+
// Validate accessibility - private middleware should be ignored or have [FoundatioIgnore]
143+
if (classSymbol.DeclaredAccessibility == Accessibility.Private && !classSymbol.HasIgnoreAttribute(context.SemanticModel.Compilation))
144+
{
145+
diagnostics.Add(new DiagnosticInfo
146+
{
147+
Identifier = "FMED006",
148+
Title = "Private Middleware Not Allowed",
149+
Message = $"Middleware '{classSymbol.Name}' is private and cannot be used. Either make it internal or public, or mark it with [FoundatioIgnore] if it should not be discovered as middleware.",
150+
Severity = DiagnosticSeverity.Error,
151+
Location = LocationInfo.CreateFrom(classDeclaration)
152+
});
153+
}
154+
142155
int? order = null;
143156

144157
// First check [Middleware(order)] attribute
@@ -171,6 +184,8 @@ public static bool IsMatch(SyntaxNode node)
171184
FinallyMethod = finallyMethod != null ? CreateMiddlewareMethodInfo(finallyMethod, context.SemanticModel.Compilation) : null,
172185
IsStatic = isStatic,
173186
Order = order,
187+
DeclaredAccessibility = classSymbol.DeclaredAccessibility,
188+
AssemblyName = classSymbol.ContainingAssembly.Name,
174189
Diagnostics = new(diagnostics.ToArray()),
175190
};
176191
}

src/Foundatio.Mediator/Models/MiddlewareInfo.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Foundatio.Mediator.Utility;
2+
using Microsoft.CodeAnalysis;
23

34
namespace Foundatio.Mediator.Models;
45

@@ -13,6 +14,8 @@ internal readonly record struct MiddlewareInfo
1314
public bool IsStatic { get; init; }
1415
public bool IsAsync => BeforeMethod?.IsAsync == true || AfterMethod?.IsAsync == true || FinallyMethod?.IsAsync == true;
1516
public int? Order { get; init; }
17+
public Accessibility DeclaredAccessibility { get; init; }
18+
public string AssemblyName { get; init; }
1619
public EquatableArray<DiagnosticInfo> Diagnostics { get; init; }
1720
}
1821

tests/Foundatio.Mediator.Tests/CrossAssemblyMiddlewareTests.cs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,106 @@ public void Handle(TestMessage msg, CancellationToken ct) { }
142142
Assert.Contains("ImplicitMiddleware.TimingMiddleware.Finally", wrapper.Source);
143143
}
144144

145+
[Fact]
146+
public void InternalMiddlewareNotDiscoveredFromReferencedAssembly()
147+
{
148+
// Internal middleware in referenced assembly should NOT be discovered
149+
var middlewareSource = """
150+
using Foundatio.Mediator;
151+
152+
[assembly: FoundatioModule]
153+
154+
namespace SharedMiddleware;
155+
156+
[Middleware]
157+
internal static class InternalMiddleware
158+
{
159+
public static void Before(object message) { }
160+
}
161+
162+
[Middleware]
163+
public static class PublicMiddleware
164+
{
165+
public static void After(object message) { }
166+
}
167+
""";
168+
169+
var middlewareCompilation = CreateMiddlewareAssembly(middlewareSource);
170+
171+
var handlerSource = """
172+
using System.Threading;
173+
using Foundatio.Mediator;
174+
175+
public record TestMessage;
176+
177+
public class TestHandler
178+
{
179+
public void Handle(TestMessage msg, CancellationToken ct) { }
180+
}
181+
""";
182+
183+
var (_, _, trees) = RunGenerator(handlerSource, [new MediatorGenerator()], additionalReferences: [middlewareCompilation]);
184+
185+
var wrapper = trees.FirstOrDefault(t => t.HintName.EndsWith("_Handler.g.cs"));
186+
187+
// Internal middleware should NOT be included
188+
Assert.DoesNotContain("InternalMiddleware", wrapper.Source);
189+
190+
// Public middleware should be included
191+
Assert.Contains("SharedMiddleware.PublicMiddleware.After", wrapper.Source);
192+
}
193+
194+
[Fact]
195+
public void PrivateMiddlewareNotDiscoveredFromReferencedAssembly()
196+
{
197+
// Private middleware in referenced assembly should NOT be discovered
198+
var middlewareSource = """
199+
using Foundatio.Mediator;
200+
201+
[assembly: FoundatioModule]
202+
203+
namespace SharedMiddleware;
204+
205+
public class Container
206+
{
207+
private class PrivateMiddleware
208+
{
209+
public static void Before(object message) { }
210+
}
211+
}
212+
213+
[Middleware]
214+
public static class PublicMiddleware
215+
{
216+
public static void After(object message) { }
217+
}
218+
""";
219+
220+
var middlewareCompilation = CreateMiddlewareAssembly(middlewareSource);
221+
222+
var handlerSource = """
223+
using System.Threading;
224+
using Foundatio.Mediator;
225+
226+
public record TestMessage;
227+
228+
public class TestHandler
229+
{
230+
public void Handle(TestMessage msg, CancellationToken ct) { }
231+
}
232+
""";
233+
234+
var (_, _, trees) = RunGenerator(handlerSource, [new MediatorGenerator()], additionalReferences: [middlewareCompilation]);
235+
236+
var wrapper = trees.FirstOrDefault(t => t.HintName.EndsWith("_Handler.g.cs"));
237+
238+
// Private middleware should NOT be included
239+
Assert.DoesNotContain("PrivateMiddleware", wrapper.Source);
240+
241+
// Public middleware should be included
242+
Assert.Contains("SharedMiddleware.PublicMiddleware.After", wrapper.Source);
243+
}
244+
145245
private static MetadataReference CreateMiddlewareAssembly(string source)
146246
{
147247
var parseOptions = new CSharpParseOptions(LanguageVersion.CSharp11);

tests/Foundatio.Mediator.Tests/DiagnosticValidationTests.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,68 @@ public static async Task Call<T>(IMediator m, T msg) {
105105
var (_, genDiags, _) = RunGenerator(src, [ Gen ]);
106106
Assert.DoesNotContain(genDiags, d => d.Id == "FMED007");
107107
}
108+
109+
[Fact]
110+
public void FMED006_PrivateMiddlewareNotAllowed()
111+
{
112+
var src = """
113+
using System.Threading;
114+
using Foundatio.Mediator;
115+
116+
public record Msg;
117+
public class MsgHandler { public void Handle(Msg m, CancellationToken ct) { } }
118+
119+
public class Container
120+
{
121+
private class PrivateMiddleware
122+
{
123+
public static void Before(Msg m) { }
124+
}
125+
}
126+
""";
127+
128+
var (_, genDiags, _) = RunGenerator(src, [ Gen ]);
129+
Assert.Contains(genDiags, d => d.Id == "FMED006" && d.GetMessage().Contains("PrivateMiddleware"));
130+
}
131+
132+
[Fact]
133+
public void MiddlewareWithIgnoreAttribute_NoDiagnostic()
134+
{
135+
var src = """
136+
using System.Threading;
137+
using Foundatio.Mediator;
138+
139+
public record Msg;
140+
public class MsgHandler { public void Handle(Msg m, CancellationToken ct) { } }
141+
142+
[FoundatioIgnore]
143+
public class IgnoredMiddleware
144+
{
145+
public static void Before(Msg m) { }
146+
}
147+
""";
148+
149+
var (_, genDiags, _) = RunGenerator(src, [ Gen ]);
150+
Assert.DoesNotContain(genDiags, d => d.Id == "FMED006");
151+
}
152+
153+
[Fact]
154+
public void InternalMiddleware_NoError()
155+
{
156+
var src = """
157+
using System.Threading;
158+
using Foundatio.Mediator;
159+
160+
public record Msg;
161+
public class MsgHandler { public void Handle(Msg m, CancellationToken ct) { } }
162+
163+
internal static class InternalMiddleware
164+
{
165+
public static void Before(Msg m) { }
166+
}
167+
""";
168+
169+
var (_, genDiags, _) = RunGenerator(src, [ Gen ]);
170+
Assert.DoesNotContain(genDiags, d => d.Id == "FMED006");
171+
}
108172
}

0 commit comments

Comments
 (0)