diff --git a/Brighter.slnx b/Brighter.slnx
index 794ccea98c..b163ff5f02 100644
--- a/Brighter.slnx
+++ b/Brighter.slnx
@@ -242,6 +242,7 @@
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 34b9e51b87..089e81ff2b 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -78,6 +78,7 @@
+
diff --git a/samples/AsyncAPI/KafkaAsyncAPI/Program.cs b/samples/AsyncAPI/KafkaAsyncAPI/Program.cs
index b0dd624236..450224bd38 100644
--- a/samples/AsyncAPI/KafkaAsyncAPI/Program.cs
+++ b/samples/AsyncAPI/KafkaAsyncAPI/Program.cs
@@ -81,6 +81,9 @@ THE SOFTWARE. */
new()
{
Topic = new RoutingKey("order.created"),
+ // Murmur2Random is recommended: its MurmurHash2 hash spreads keys evenly across
+ // partitions, avoiding hot partitions
+ Partitioner = Partitioner.Murmur2Random,
NumPartitions = 3,
MessageSendMaxRetries = 3,
MessageTimeoutMs = 1000,
diff --git a/samples/TaskQueue/KafkaDeferOnError/GreetingsSender/Program.cs b/samples/TaskQueue/KafkaDeferOnError/GreetingsSender/Program.cs
index 05e9bc87b3..1b60daff81 100644
--- a/samples/TaskQueue/KafkaDeferOnError/GreetingsSender/Program.cs
+++ b/samples/TaskQueue/KafkaDeferOnError/GreetingsSender/Program.cs
@@ -59,6 +59,9 @@ THE SOFTWARE. */
{
Topic = new RoutingKey("greeting.event"),
RequestType = typeof(GreetingEvent),
+ // Murmur2Random is recommended: its MurmurHash2 hash spreads keys evenly across
+ // partitions, avoiding hot partitions
+ Partitioner = Partitioner.Murmur2Random,
NumPartitions = 3,
MessageSendMaxRetries = 3,
MessageTimeoutMs = 1000,
diff --git a/samples/TaskQueue/KafkaDontAckOnError/GreetingsSender/Program.cs b/samples/TaskQueue/KafkaDontAckOnError/GreetingsSender/Program.cs
index 8f7df77794..3ed5f39bf6 100644
--- a/samples/TaskQueue/KafkaDontAckOnError/GreetingsSender/Program.cs
+++ b/samples/TaskQueue/KafkaDontAckOnError/GreetingsSender/Program.cs
@@ -59,6 +59,9 @@ THE SOFTWARE. */
{
Topic = new RoutingKey("greeting.event"),
RequestType = typeof(GreetingEvent),
+ // Murmur2Random is recommended: its MurmurHash2 hash spreads keys evenly across
+ // partitions, avoiding hot partitions
+ Partitioner = Partitioner.Murmur2Random,
NumPartitions = 3,
MessageSendMaxRetries = 3,
MessageTimeoutMs = 1000,
diff --git a/samples/TaskQueue/KafkaDynamicEventStream/TaskStatusSender/Program.cs b/samples/TaskQueue/KafkaDynamicEventStream/TaskStatusSender/Program.cs
index d46a0d5caa..bacce93f23 100644
--- a/samples/TaskQueue/KafkaDynamicEventStream/TaskStatusSender/Program.cs
+++ b/samples/TaskQueue/KafkaDynamicEventStream/TaskStatusSender/Program.cs
@@ -52,6 +52,9 @@ THE SOFTWARE. */
//the same topic for both TaskCreated and TaskUpdated, but different cloud events types
Topic = new RoutingKey("task.update"),
Type = new CloudEventsType("io.goparamore.task.created"),
+ // Murmur2Random is recommended: its MurmurHash2 hash spreads keys evenly across
+ // partitions, avoiding hot partitions
+ Partitioner = Partitioner.Murmur2Random,
NumPartitions = 3,
MessageSendMaxRetries = 3,
MessageTimeoutMs = 1000,
@@ -61,6 +64,9 @@ THE SOFTWARE. */
{
Topic = new RoutingKey("task.update"),
Type = new CloudEventsType("io.goparamore.task.updated"),
+ // Murmur2Random is recommended: its MurmurHash2 hash spreads keys evenly across
+ // partitions, avoiding hot partitions
+ Partitioner = Partitioner.Murmur2Random,
NumPartitions = 3,
MessageSendMaxRetries = 3,
MessageTimeoutMs = 1000,
diff --git a/samples/TaskQueue/KafkaSchemaRegistry/GreetingsSender/Program.cs b/samples/TaskQueue/KafkaSchemaRegistry/GreetingsSender/Program.cs
index addd77d556..8814c3416f 100644
--- a/samples/TaskQueue/KafkaSchemaRegistry/GreetingsSender/Program.cs
+++ b/samples/TaskQueue/KafkaSchemaRegistry/GreetingsSender/Program.cs
@@ -62,6 +62,9 @@ THE SOFTWARE. */
{
Topic = new RoutingKey("greeting.event"),
RequestType = typeof(GreetingEvent),
+ // Murmur2Random is recommended: its MurmurHash2 hash spreads keys evenly across
+ // partitions, avoiding hot partitions
+ Partitioner = Partitioner.Murmur2Random,
MessageSendMaxRetries = 3,
MessageTimeoutMs = 1000,
MaxInFlightRequestsPerConnection = 1
diff --git a/samples/TaskQueue/KafkaTaskQueue/GreetingsSender/Program.cs b/samples/TaskQueue/KafkaTaskQueue/GreetingsSender/Program.cs
index 26de70c991..0aa04092f8 100644
--- a/samples/TaskQueue/KafkaTaskQueue/GreetingsSender/Program.cs
+++ b/samples/TaskQueue/KafkaTaskQueue/GreetingsSender/Program.cs
@@ -80,6 +80,9 @@ THE SOFTWARE. */
{
Topic = new RoutingKey("greeting.event"),
RequestType = typeof(GreetingEvent),
+ // Murmur2Random is recommended: its MurmurHash2 hash spreads keys evenly across
+ // partitions, avoiding hot partitions
+ Partitioner = Partitioner.Murmur2Random,
NumPartitions = 3,
MessageSendMaxRetries = 3,
MessageTimeoutMs = 1000,
diff --git a/samples/TaskQueue/KafkaTaskQueueWithDLQ/GreetingsSender/Program.cs b/samples/TaskQueue/KafkaTaskQueueWithDLQ/GreetingsSender/Program.cs
index 93f5444c43..badb8dd4fc 100644
--- a/samples/TaskQueue/KafkaTaskQueueWithDLQ/GreetingsSender/Program.cs
+++ b/samples/TaskQueue/KafkaTaskQueueWithDLQ/GreetingsSender/Program.cs
@@ -96,6 +96,9 @@ THE SOFTWARE. */
{
Topic = new RoutingKey("greeting.event"),
RequestType = typeof(GreetingEvent),
+ // Murmur2Random is recommended: its MurmurHash2 hash spreads keys evenly across
+ // partitions, avoiding hot partitions
+ Partitioner = Partitioner.Murmur2Random,
NumPartitions = 3,
MessageSendMaxRetries = 3,
MessageTimeoutMs = 1000,
diff --git a/samples/TaskQueue/MultiBus/GreetingsSender/Program.cs b/samples/TaskQueue/MultiBus/GreetingsSender/Program.cs
index f389e4503b..6bc7c81333 100644
--- a/samples/TaskQueue/MultiBus/GreetingsSender/Program.cs
+++ b/samples/TaskQueue/MultiBus/GreetingsSender/Program.cs
@@ -72,6 +72,9 @@ THE SOFTWARE. */
{
Topic = new RoutingKey("greeting.event"),
RequestType = typeof(GreetingEvent),
+ // Murmur2Random is recommended: its MurmurHash2 hash spreads keys evenly across
+ // partitions, avoiding hot partitions
+ Partitioner = Partitioner.Murmur2Random,
NumPartitions = 3,
MessageSendMaxRetries = 3,
MessageTimeoutMs = 1000,
diff --git a/samples/WebAPI/WebAPI_Common/TransportMaker/ConfigureTransport.cs b/samples/WebAPI/WebAPI_Common/TransportMaker/ConfigureTransport.cs
index 3730b9b839..bda408018f 100644
--- a/samples/WebAPI/WebAPI_Common/TransportMaker/ConfigureTransport.cs
+++ b/samples/WebAPI/WebAPI_Common/TransportMaker/ConfigureTransport.cs
@@ -97,6 +97,9 @@ public static IAmAProducerRegistry GetKafkaProducerRegistry() where T: class,
{
Topic = new RoutingKey(typeof(T).Name),
RequestType = typeof(T),
+ // Murmur2Random is recommended: its MurmurHash2 hash spreads keys evenly across
+ // partitions, avoiding hot partitions
+ Partitioner = Paramore.Brighter.MessagingGateway.Kafka.Partitioner.Murmur2Random,
MessageSendMaxRetries = 3,
MessageTimeoutMs = 1000,
MaxInFlightRequestsPerConnection = 1,
diff --git a/src/Paramore.Brighter.Analyzer.CodeFixes/CodeFixes/MissingPartitionerCodeFixProvider.cs b/src/Paramore.Brighter.Analyzer.CodeFixes/CodeFixes/MissingPartitionerCodeFixProvider.cs
new file mode 100644
index 0000000000..e97a2bac50
--- /dev/null
+++ b/src/Paramore.Brighter.Analyzer.CodeFixes/CodeFixes/MissingPartitionerCodeFixProvider.cs
@@ -0,0 +1,204 @@
+#region License
+/* The MIT License (MIT)
+Copyright © 2026 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the “Software”), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Formatting;
+using Microsoft.CodeAnalysis.Simplification;
+
+namespace Paramore.Brighter.Analyzer.CodeFixes;
+
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MissingPartitionerCodeFixProvider)), Shared]
+public class MissingPartitionerCodeFixProvider : CodeFixProvider
+{
+ public override ImmutableArray FixableDiagnosticIds => [DiagnosticsIds.MissingPartitioner];
+
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root == null)
+ {
+ return;
+ }
+
+ foreach (var diagnostic in context.Diagnostics)
+ {
+ var objectCreation = root.FindNode(diagnostic.Location.SourceSpan)
+ .AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault();
+
+ if (objectCreation == null)
+ {
+ continue;
+ }
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: $"Set 'Partitioner' to 'Partitioner.{BrighterAnalyzerGlobals.Murmur2RandomPartitionerValue}' (re-partitions the topic)",
+ createChangedDocument: ct => AddPartitionerAsync(context.Document, objectCreation, ct),
+ equivalenceKey: nameof(MissingPartitionerCodeFixProvider)),
+ diagnostic);
+ }
+ }
+
+ private static async Task AddPartitionerAsync(
+ Document document,
+ BaseObjectCreationExpressionSyntax objectCreation,
+ CancellationToken cancellationToken)
+ {
+ var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+
+ var assignment = SyntaxFactory.AssignmentExpression(
+ SyntaxKind.SimpleAssignmentExpression,
+ SyntaxFactory.IdentifierName(BrighterAnalyzerGlobals.PartitionerProperty),
+ SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ SyntaxFactory.ParseExpression($"{BrighterAnalyzerGlobals.KafkaNamespace}.{BrighterAnalyzerGlobals.PartitionerEnum}"),
+ SyntaxFactory.IdentifierName(BrighterAnalyzerGlobals.Murmur2RandomPartitionerValue))
+ .WithAdditionalAnnotations(Simplifier.Annotation));
+
+ InitializerExpressionSyntax initializer;
+ if (objectCreation.Initializer == null)
+ {
+ initializer = SyntaxFactory.InitializerExpression(
+ SyntaxKind.ObjectInitializerExpression,
+ SyntaxFactory.SingletonSeparatedList(assignment));
+ }
+ else if (objectCreation.Initializer.Expressions.Count == 0)
+ {
+ // new KafkaPublication { } — keep the existing (empty) braces and trivia.
+ initializer = objectCreation.Initializer.WithExpressions(
+ SyntaxFactory.SingletonSeparatedList(assignment));
+ }
+ else
+ {
+ initializer = AddInitializerExpression(objectCreation.Initializer, assignment);
+ }
+
+ var newObjectCreation = objectCreation.WithInitializer(initializer);
+
+ // Drop a now-redundant empty argument list: with an initializer present,
+ // new KafkaPublication { ... } reads better than new KafkaPublication() { ... }.
+ // Keep the argument list's trailing trivia (the space before the brace).
+ if (newObjectCreation is ObjectCreationExpressionSyntax { ArgumentList.Arguments.Count: 0 } explicitCreation)
+ {
+ newObjectCreation = explicitCreation
+ .WithType(explicitCreation.Type.WithTrailingTrivia(explicitCreation.ArgumentList.GetTrailingTrivia()))
+ .WithArgumentList(null);
+ }
+
+ newObjectCreation = newObjectCreation.WithAdditionalAnnotations(Formatter.Annotation);
+
+ var newRoot = root!.ReplaceNode(objectCreation, newObjectCreation);
+ var formatted = await Formatter.FormatAsync(document.WithSyntaxRoot(newRoot), Formatter.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ // Reduce the fully qualified Partitioner reference where the using is
+ // already present; otherwise keep it qualified so the fix always compiles.
+ return await Simplifier.ReduceAsync(formatted, Simplifier.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+
+ private static InitializerExpressionSyntax AddInitializerExpression(
+ InitializerExpressionSyntax initializer,
+ ExpressionSyntax expression)
+ {
+ // Build the new expression list by hand: AddExpressions would insert the
+ // separator comma right after the last expression but before its trailing
+ // trivia, so the comma lands on the wrong line and any trailing comment
+ // (e.g. "// one per shard") would move onto the new expression.
+ var nodesAndTokens = initializer.Expressions.GetWithSeparators();
+
+ // A trailing comma ("{ A = 1, B = 2, }") carries the closing-brace trivia:
+ // drop the comma and move its trivia onto the last expression.
+ if (nodesAndTokens.Count > 0 && nodesAndTokens[nodesAndTokens.Count - 1].IsToken)
+ {
+ var trailingComma = nodesAndTokens[nodesAndTokens.Count - 1].AsToken();
+ nodesAndTokens = nodesAndTokens.RemoveAt(nodesAndTokens.Count - 1);
+ nodesAndTokens = nodesAndTokens.Replace(
+ nodesAndTokens[nodesAndTokens.Count - 1],
+ ((ExpressionSyntax)nodesAndTokens[nodesAndTokens.Count - 1].AsNode()!)
+ .WithTrailingTrivia(trailingComma.TrailingTrivia));
+ }
+
+ var lastExpression = (ExpressionSyntax)nodesAndTokens[nodesAndTokens.Count - 1].AsNode()!;
+ var trailingTrivia = lastExpression.GetTrailingTrivia();
+ var beforeEndOfLine = trailingTrivia.TakeWhile(t => !t.IsKind(SyntaxKind.EndOfLineTrivia)).ToList();
+ var fromEndOfLine = trailingTrivia.SkipWhile(t => !t.IsKind(SyntaxKind.EndOfLineTrivia)).ToList();
+
+ var separator = SyntaxFactory.Token(SyntaxKind.CommaToken);
+ ExpressionSyntax newExpression;
+ if (fromEndOfLine.Count == 0)
+ {
+ // Single-line initializer ("{ Topic = x }"): keep it on one line,
+ // with single spaces around the new expression.
+ var hasComment = beforeEndOfLine.Any(IsComment);
+ if (hasComment)
+ {
+ separator = separator.WithTrailingTrivia(beforeEndOfLine);
+ }
+
+ newExpression = expression
+ .WithLeadingTrivia(SyntaxFactory.TriviaList(SyntaxFactory.Space))
+ .WithTrailingTrivia(hasComment
+ ? SyntaxFactory.TriviaList(SyntaxFactory.Space)
+ : SyntaxFactory.TriviaList(beforeEndOfLine));
+ }
+ else
+ {
+ // Multi-line: comments stay on their line attached to the comma, the
+ // newline + indent that follows an existing separator (or the open
+ // brace) leads the new expression, and the newline before the closing
+ // brace moves behind it.
+ separator = separator.WithTrailingTrivia(beforeEndOfLine);
+ var leadingTrivia = nodesAndTokens.Count > 1
+ ? nodesAndTokens[nodesAndTokens.Count - 2].AsToken().TrailingTrivia
+ : initializer.OpenBraceToken.TrailingTrivia;
+ newExpression = expression
+ .WithLeadingTrivia(leadingTrivia)
+ .WithTrailingTrivia(fromEndOfLine);
+ }
+
+ return initializer.WithExpressions(
+ SyntaxFactory.SeparatedList(
+ nodesAndTokens
+ .Replace(nodesAndTokens[nodesAndTokens.Count - 1], lastExpression.WithoutTrailingTrivia())
+ .Add(separator)
+ .Add(newExpression)));
+ }
+
+ private static bool IsComment(SyntaxTrivia trivia)
+ {
+ return trivia.IsKind(SyntaxKind.SingleLineCommentTrivia) || trivia.IsKind(SyntaxKind.MultiLineCommentTrivia);
+ }
+}
diff --git a/src/Paramore.Brighter.Analyzer.CodeFixes/CodeFixes/PartitionerValueCodeFixProvider.cs b/src/Paramore.Brighter.Analyzer.CodeFixes/CodeFixes/PartitionerValueCodeFixProvider.cs
new file mode 100644
index 0000000000..44e3e96b83
--- /dev/null
+++ b/src/Paramore.Brighter.Analyzer.CodeFixes/CodeFixes/PartitionerValueCodeFixProvider.cs
@@ -0,0 +1,118 @@
+#region License
+/* The MIT License (MIT)
+Copyright © 2026 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the “Software”), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Paramore.Brighter.Analyzer.CodeFixes;
+
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(PartitionerValueCodeFixProvider)), Shared]
+public class PartitionerValueCodeFixProvider : CodeFixProvider
+{
+ public override ImmutableArray FixableDiagnosticIds => [DiagnosticsIds.ConsistentRandomPartitioner, DiagnosticsIds.ConsistentPartitioner];
+
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
+ if (root == null)
+ {
+ return;
+ }
+
+ foreach (var diagnostic in context.Diagnostics)
+ {
+ var target = diagnostic.Id == DiagnosticsIds.ConsistentRandomPartitioner
+ ? BrighterAnalyzerGlobals.Murmur2RandomPartitionerValue
+ : BrighterAnalyzerGlobals.Murmur2PartitionerValue;
+
+ var assignment = root.FindNode(diagnostic.Location.SourceSpan)
+ .DescendantNodesAndSelf()
+ .OfType()
+ .FirstOrDefault(a => a.Left switch
+ {
+ // Partitioner = ... (object initializer) or publication.Partitioner = ... (post-construction)
+ IdentifierNameSyntax id => id.Identifier.ValueText == BrighterAnalyzerGlobals.PartitionerProperty,
+ MemberAccessExpressionSyntax memberAccess => memberAccess.Name.Identifier.ValueText == BrighterAnalyzerGlobals.PartitionerProperty,
+ _ => false
+ });
+
+ if (assignment == null || !CanRewrite(assignment.Right))
+ {
+ // Only a member access (Partitioner.Consistent), a parenthesized
+ // member access, or a bare identifier (using static) can be
+ // rewritten safely; anything else (e.g. a cast) would not
+ // compile after the fix, so don't offer one.
+ continue;
+ }
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: $"Use 'Partitioner.{target}' (re-partitions the topic)",
+ createChangedDocument: ct => ReplacePartitionerValueAsync(context.Document, assignment, target, ct),
+ equivalenceKey: $"{nameof(PartitionerValueCodeFixProvider)}:{target}"),
+ diagnostic);
+ }
+ }
+
+ private static bool CanRewrite(ExpressionSyntax right)
+ {
+ return right is MemberAccessExpressionSyntax
+ or IdentifierNameSyntax
+ or ParenthesizedExpressionSyntax { Expression: MemberAccessExpressionSyntax };
+ }
+
+ private static async Task ReplacePartitionerValueAsync(
+ Document document,
+ AssignmentExpressionSyntax assignment,
+ string target,
+ CancellationToken cancellationToken)
+ {
+ var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
+
+ var newName = SyntaxFactory.IdentifierName(target);
+ ExpressionSyntax newValue = assignment.Right switch
+ {
+ MemberAccessExpressionSyntax memberAccess => memberAccess.WithName(newName),
+ ParenthesizedExpressionSyntax { Expression: MemberAccessExpressionSyntax memberAccess } parenthesized =>
+ parenthesized.WithExpression(memberAccess.WithName(newName)),
+ _ => newName
+ };
+
+ var newRoot = root!.ReplaceNode(
+ assignment.Right,
+ newValue.WithTriviaFrom(assignment.Right));
+
+ return document.WithSyntaxRoot(newRoot);
+ }
+}
diff --git a/src/Paramore.Brighter.Analyzer.CodeFixes/Paramore.Brighter.Analyzer.CodeFixes.csproj b/src/Paramore.Brighter.Analyzer.CodeFixes/Paramore.Brighter.Analyzer.CodeFixes.csproj
new file mode 100644
index 0000000000..57966b1e48
--- /dev/null
+++ b/src/Paramore.Brighter.Analyzer.CodeFixes/Paramore.Brighter.Analyzer.CodeFixes.csproj
@@ -0,0 +1,20 @@
+
+
+
+ $(BrighterNetStandardTargetFrameworks)
+ false
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Paramore.Brighter.Analyzer.Package/Paramore.Brighter.Analyzer.Package.csproj b/src/Paramore.Brighter.Analyzer.Package/Paramore.Brighter.Analyzer.Package.csproj
index 23ffadb7c7..a36fab7d16 100644
--- a/src/Paramore.Brighter.Analyzer.Package/Paramore.Brighter.Analyzer.Package.csproj
+++ b/src/Paramore.Brighter.Analyzer.Package/Paramore.Brighter.Analyzer.Package.csproj
@@ -20,15 +20,21 @@
+
+
+
+
+
diff --git a/src/Paramore.Brighter.Analyzer.Package/build/Paramore.Brighter.Analyzer.Package.props b/src/Paramore.Brighter.Analyzer.Package/build/Paramore.Brighter.Analyzer.Package.props
new file mode 100644
index 0000000000..8f7bec5577
--- /dev/null
+++ b/src/Paramore.Brighter.Analyzer.Package/build/Paramore.Brighter.Analyzer.Package.props
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Paramore.Brighter.Analyzer.Package/build/Paramore.Brighter.Analyzer.props b/src/Paramore.Brighter.Analyzer.Package/build/Paramore.Brighter.Analyzer.props
deleted file mode 100644
index ead6e08f09..0000000000
--- a/src/Paramore.Brighter.Analyzer.Package/build/Paramore.Brighter.Analyzer.props
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Paramore.Brighter.Analyzer.Package/configuration/default.editorconfig b/src/Paramore.Brighter.Analyzer.Package/configuration/default.editorconfig
index c4458e30f1..609f507acb 100644
--- a/src/Paramore.Brighter.Analyzer.Package/configuration/default.editorconfig
+++ b/src/Paramore.Brighter.Analyzer.Package/configuration/default.editorconfig
@@ -1,5 +1,10 @@
+is_global = true
+
dotnet_diagnostic.BRT001.severity = warning
dotnet_diagnostic.BRT002.severity = warning
dotnet_diagnostic.BRT003.severity = warning
dotnet_diagnostic.BRT004.severity = warning
-dotnet_diagnostic.BRT005.severity = warning
\ No newline at end of file
+dotnet_diagnostic.BRT005.severity = warning
+dotnet_diagnostic.BRT006.severity = warning
+dotnet_diagnostic.BRT007.severity = warning
+dotnet_diagnostic.BRT008.severity = warning
diff --git a/src/Paramore.Brighter.Analyzer.Package/configuration/none.editorconfig b/src/Paramore.Brighter.Analyzer.Package/configuration/none.editorconfig
index aef953fc0e..0b5d80477c 100644
--- a/src/Paramore.Brighter.Analyzer.Package/configuration/none.editorconfig
+++ b/src/Paramore.Brighter.Analyzer.Package/configuration/none.editorconfig
@@ -1,5 +1,10 @@
+is_global = true
+
dotnet_diagnostic.BRT001.severity = none
dotnet_diagnostic.BRT002.severity = none
dotnet_diagnostic.BRT003.severity = none
dotnet_diagnostic.BRT004.severity = none
-dotnet_diagnostic.BRT005.severity = none
\ No newline at end of file
+dotnet_diagnostic.BRT005.severity = none
+dotnet_diagnostic.BRT006.severity = none
+dotnet_diagnostic.BRT007.severity = none
+dotnet_diagnostic.BRT008.severity = none
diff --git a/src/Paramore.Brighter.Analyzer/AnalyzerReleases.Shipped.md b/src/Paramore.Brighter.Analyzer/AnalyzerReleases.Shipped.md
index a32c29b2e2..f92f0d2f08 100644
--- a/src/Paramore.Brighter.Analyzer/AnalyzerReleases.Shipped.md
+++ b/src/Paramore.Brighter.Analyzer/AnalyzerReleases.Shipped.md
@@ -8,4 +8,7 @@ BRT001 | Design | Warning | ([BRT001](./docs/BRT001.md)) Request Type assign
BRT002 | Design | Warning | ([BRT002](./docs/BRT002.md)) RequestType is not child of IRequest
BRT003 | Design | Warning | ([BRT003](./docs/BRT003.md)) MessagePump assignment is Missing
BRT004 | Design | Warning | ([BRT004](./docs/BRT004.md)) Wrap attribute is applied to wrong Method
-BRT005 | Design | Warning | ([BRT005](./docs/BRT005.md)) UnWrap attribute is applied to wrong Method
\ No newline at end of file
+BRT005 | Design | Warning | ([BRT005](./docs/BRT005.md)) UnWrap attribute is applied to wrong Method
+BRT006 | Design | Warning | ([BRT006](./docs/BRT006.md)) Missing Partitioner assignment
+BRT007 | Design | Warning | ([BRT007](./docs/BRT007.md)) ConsistentRandom Partitioner Used
+BRT008 | Design | Warning | ([BRT008](./docs/BRT008.md)) Consistent Partitioner Used
diff --git a/src/Paramore.Brighter.Analyzer/Analyzers/KafkaPublicationPartitionerAnalyzer.cs b/src/Paramore.Brighter.Analyzer/Analyzers/KafkaPublicationPartitionerAnalyzer.cs
new file mode 100644
index 0000000000..003949cd5f
--- /dev/null
+++ b/src/Paramore.Brighter.Analyzer/Analyzers/KafkaPublicationPartitionerAnalyzer.cs
@@ -0,0 +1,335 @@
+#region License
+
+/* The MIT License (MIT)
+Copyright © 2026 Ian Cooper
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the “Software”), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+using System.Collections.Concurrent;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+using Paramore.Brighter.Analyzer.Visitors.Operation;
+
+namespace Paramore.Brighter.Analyzer.Analyzers;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class KafkaPublicationPartitionerAnalyzer : DiagnosticAnalyzer
+{
+ private const string PartitionerCategory = "Design";
+
+ public static readonly DiagnosticDescriptor MissingPartitionerRule = new(
+ id: DiagnosticsIds.MissingPartitioner,
+ title: "Missing Partitioner",
+ messageFormat: "Partitioner assignment is missing from {0}. Consider setting it explicitly.",
+ category: PartitionerCategory,
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "Setting the Partitioner explicitly makes the choice visible. Be aware that changing the partitioner re-partitions the topic; existing publications can keep the implicit default to preserve their current partition assignment.",
+ helpLinkUri: "https://github.com/BrighterCommand/Brighter/blob/master/src/Paramore.Brighter.Analyzer/docs/BRT006.md"
+ );
+
+ public static readonly DiagnosticDescriptor ConsistentRandomPartitionerRule = new(
+ id: DiagnosticsIds.ConsistentRandomPartitioner,
+ title: "ConsistentRandom Partitioner Used",
+ messageFormat: "Prefer 'Murmur2Random' over 'ConsistentRandom' for new KafkaPublications",
+ category: PartitionerCategory,
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "'Murmur2Random' spreads keys more evenly across partitions than the CRC32-based 'ConsistentRandom', avoiding hot partitions. Existing publications can keep 'ConsistentRandom' to preserve their current partition assignment.",
+ helpLinkUri: "https://github.com/BrighterCommand/Brighter/blob/master/src/Paramore.Brighter.Analyzer/docs/BRT007.md"
+ );
+
+ public static readonly DiagnosticDescriptor ConsistentPartitionerRule = new(
+ id: DiagnosticsIds.ConsistentPartitioner,
+ title: "Consistent Partitioner Used",
+ messageFormat: "Prefer 'Murmur2' over 'Consistent' for new KafkaPublications",
+ category: PartitionerCategory,
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true,
+ description: "'Murmur2' spreads keys more evenly across partitions than the CRC32-based 'Consistent', avoiding hot partitions. Existing publications can keep 'Consistent' to preserve their current partition assignment.",
+ helpLinkUri: "https://github.com/BrighterCommand/Brighter/blob/master/src/Paramore.Brighter.Analyzer/docs/BRT008.md"
+ );
+
+ public override ImmutableArray SupportedDiagnostics => [MissingPartitionerRule, ConsistentRandomPartitionerRule, ConsistentPartitionerRule];
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterCompilationStartAction(compilationContext =>
+ {
+ // Solutions that don't reference the Kafka gateway can never create a
+ // KafkaPublication; don't pay for an operation callback there at all.
+ // Resolve the symbols once and compare by symbol from here on.
+ var kafkaPublicationSymbol = compilationContext.Compilation.GetTypeByMetadataName(
+ $"{BrighterAnalyzerGlobals.KafkaNamespace}.{BrighterAnalyzerGlobals.KafkaPublicationClassName}");
+ var partitionerEnumSymbol = compilationContext.Compilation.GetTypeByMetadataName(
+ $"{BrighterAnalyzerGlobals.KafkaNamespace}.{BrighterAnalyzerGlobals.PartitionerEnum}");
+ if (kafkaPublicationSymbol == null || partitionerEnumSymbol == null)
+ {
+ return;
+ }
+
+ // Memoises the constructor inspection per type, so a subclass
+ // instantiated in many places is only walked once per compilation.
+ var constructorCheckCache = new ConcurrentDictionary(SymbolEqualityComparer.Default);
+
+ compilationContext.RegisterOperationAction(
+ operationContext => AnalyzerObjectCreation(operationContext, kafkaPublicationSymbol, partitionerEnumSymbol, constructorCheckCache),
+ OperationKind.ObjectCreation);
+ compilationContext.RegisterOperationAction(
+ operationContext => AnalyzeAssignment(operationContext, kafkaPublicationSymbol, partitionerEnumSymbol),
+ OperationKind.SimpleAssignment);
+ });
+ }
+
+ private static void AnalyzerObjectCreation(
+ OperationAnalysisContext context,
+ INamedTypeSymbol kafkaPublicationSymbol,
+ INamedTypeSymbol partitionerEnumSymbol,
+ ConcurrentDictionary constructorCheckCache)
+ {
+ var operation = (IObjectCreationOperation)context.Operation;
+
+ // Cheap rejection before allocating the visitor; most object creations
+ // in a compilation are not KafkaPublications.
+ if (!KafkaPublicationPartitionerVisitor.IsKafkaPublicationType(operation.Type, kafkaPublicationSymbol))
+ {
+ return;
+ }
+
+ var visitor = new KafkaPublicationPartitionerVisitor(kafkaPublicationSymbol, partitionerEnumSymbol);
+ operation.Accept(visitor);
+
+ if (!visitor.IsPartitionerAssigned)
+ {
+ if (HasPartitionerAssignmentAfterConstruction(operation) ||
+ SetsPartitionerInConstructor(operation.Type, kafkaPublicationSymbol, constructorCheckCache, context.CancellationToken))
+ {
+ // The partitioner is set on the instance after construction or by
+ // the type's own constructor; any discouraged value is reported
+ // by AnalyzeAssignment instead.
+ return;
+ }
+
+ context.ReportDiagnostic(Diagnostic.Create(
+ MissingPartitionerRule,
+ GetCreationLocation(operation.Syntax),
+ visitor.PublicationName));
+ }
+ else if (visitor.IsConsistentRandom)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ ConsistentRandomPartitionerRule,
+ visitor.PartitionerAssignmentLocation));
+ }
+ else if (visitor.IsConsistent)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ ConsistentPartitionerRule,
+ visitor.PartitionerAssignmentLocation));
+ }
+ }
+
+ // Report on the type name (or the `new` keyword for a target-typed new)
+ // rather than the whole creation, so a large initializer isn't squiggled
+ // in full.
+ private static Location GetCreationLocation(SyntaxNode creationSyntax)
+ {
+ return creationSyntax switch
+ {
+ ObjectCreationExpressionSyntax objectCreation => objectCreation.Type.GetLocation(),
+ ImplicitObjectCreationExpressionSyntax implicitCreation => implicitCreation.NewKeyword.GetLocation(),
+ _ => creationSyntax.GetLocation()
+ };
+ }
+
+ // A subclass can set the partitioner in its own constructor, e.g.:
+ // class OrdersPublication : KafkaPublication
+ // {
+ // public OrdersPublication() { Partitioner = Partitioner.Murmur2Random; }
+ // }
+ // Don't report BRT006 for such a type — an initializer added by the code fix
+ // would override the subclass's deliberate choice. Only constructors declared
+ // below KafkaPublication itself are considered; its own Partitioner default is
+ // exactly what BRT006 flags as implicit.
+ private static bool SetsPartitionerInConstructor(
+ ITypeSymbol? type,
+ INamedTypeSymbol kafkaPublicationSymbol,
+ ConcurrentDictionary constructorCheckCache,
+ CancellationToken cancellationToken)
+ {
+ for (var current = type as INamedTypeSymbol;
+ current != null && !SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, kafkaPublicationSymbol);
+ current = current.BaseType)
+ {
+ var assigns = constructorCheckCache.GetOrAdd(
+ current,
+ symbol => symbol.InstanceConstructors.Any(constructor => ConstructorAssignsPartitioner(constructor, cancellationToken)));
+
+ if (assigns)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // Syntactic check (analyzers must not call Compilation.GetSemanticModel, RS1030):
+ // an assignment to `Partitioner` or `this.Partitioner` in the constructor body.
+ // In a KafkaPublication subclass constructor an unqualified `Partitioner` can
+ // only bind to the inherited property or a local of the same name — the latter
+ // is contrived and accepted.
+ private static bool ConstructorAssignsPartitioner(IMethodSymbol constructor, CancellationToken cancellationToken)
+ {
+ foreach (var syntaxReference in constructor.DeclaringSyntaxReferences)
+ {
+ var assignsPartitioner = syntaxReference.GetSyntax(cancellationToken)
+ .DescendantNodes()
+ .OfType()
+ .Any(assignment => assignment.Left switch
+ {
+ IdentifierNameSyntax id => id.Identifier.ValueText == BrighterAnalyzerGlobals.PartitionerProperty,
+ MemberAccessExpressionSyntax { Expression: ThisExpressionSyntax } memberAccess =>
+ memberAccess.Name.Identifier.ValueText == BrighterAnalyzerGlobals.PartitionerProperty,
+ _ => false
+ });
+
+ if (assignsPartitioner)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // Reports discouraged partitioner values assigned outside an object
+ // initializer, e.g.:
+ // var publication = new KafkaPublication();
+ // publication.Partitioner = Partitioner.Consistent;
+ // The diagnostic is reported from the assignment's own callback so it stays
+ // local to the analyzed operation.
+ private static void AnalyzeAssignment(
+ OperationAnalysisContext context,
+ INamedTypeSymbol kafkaPublicationSymbol,
+ INamedTypeSymbol partitionerEnumSymbol)
+ {
+ var assignment = (ISimpleAssignmentOperation)context.Operation;
+
+ // Assignments inside an object creation's initializer
+ // (new KafkaPublication { Partitioner = ... }) are handled by
+ // AnalyzerObjectCreation. Assignments in a nested member initializer
+ // (new Holder { Publication = { Partitioner = ... } }) — whose parent
+ // initializer hangs off an IMemberInitializerOperation, not a creation —
+ // ARE handled here.
+ if (assignment.Parent is IObjectOrCollectionInitializerOperation { Parent: not IMemberInitializerOperation })
+ {
+ return;
+ }
+
+ if (assignment.Target is not IPropertyReferenceOperation propertyReference ||
+ propertyReference.Property.Name != BrighterAnalyzerGlobals.PartitionerProperty ||
+ !KafkaPublicationPartitionerVisitor.IsKafkaPublicationType(propertyReference.Property.ContainingType, kafkaPublicationSymbol))
+ {
+ return;
+ }
+
+ switch (KafkaPublicationPartitionerVisitor.GetPartitionerValueName(assignment.Value, partitionerEnumSymbol))
+ {
+ case BrighterAnalyzerGlobals.ConsistentRandomPartitionerValue:
+ context.ReportDiagnostic(Diagnostic.Create(
+ ConsistentRandomPartitionerRule,
+ assignment.Syntax.GetLocation()));
+ break;
+ case BrighterAnalyzerGlobals.ConsistentPartitionerValue:
+ context.ReportDiagnostic(Diagnostic.Create(
+ ConsistentPartitionerRule,
+ assignment.Syntax.GetLocation()));
+ break;
+ }
+ }
+
+ // Checks whether the partitioner is assigned on the just-created instance
+ // later in the same block, e.g.:
+ // var publication = new KafkaPublication();
+ // publication.Partitioner = Partitioner.Murmur2Random;
+ // Works for locals, fields, properties and parameters, and sees assignments
+ // inside nested blocks (if/else/loops). Assignments made before the
+ // construction, or elsewhere (helper methods, other blocks), are not tracked.
+ private static bool HasPartitionerAssignmentAfterConstruction(IObjectCreationOperation operation)
+ {
+ ISymbol? symbol = operation.Parent switch
+ {
+ IVariableInitializerOperation { Parent: IVariableDeclaratorOperation declarator } => declarator.Symbol,
+ ISimpleAssignmentOperation { Target: ILocalReferenceOperation localReference } => localReference.Local,
+ ISimpleAssignmentOperation { Target: IFieldReferenceOperation fieldReference } => fieldReference.Field,
+ ISimpleAssignmentOperation { Target: IPropertyReferenceOperation propertyReference } => propertyReference.Property,
+ ISimpleAssignmentOperation { Target: IParameterReferenceOperation parameterReference } => parameterReference.Parameter,
+ _ => null
+ };
+
+ if (symbol == null)
+ {
+ return false;
+ }
+
+ var ancestor = operation.Parent;
+ while (ancestor != null && ancestor is not IBlockOperation)
+ {
+ ancestor = ancestor.Parent;
+ }
+
+ if (ancestor is not IBlockOperation block)
+ {
+ return false;
+ }
+
+ return block.Descendants()
+ .OfType()
+ // Field/property targets are compared by symbol, not instance: an
+ // assignment through another object sharing the field (a.Pub vs b.Pub)
+ // would also match — an accepted, contrived edge case.
+ .Any(assignment =>
+ assignment.Syntax.SpanStart > operation.Syntax.SpanStart &&
+ assignment.Target is IPropertyReferenceOperation propertyReference &&
+ propertyReference.Property.Name == BrighterAnalyzerGlobals.PartitionerProperty &&
+ IsReferenceTo(propertyReference.Instance, symbol));
+ }
+
+ private static bool IsReferenceTo(IOperation? instance, ISymbol symbol)
+ {
+ return instance switch
+ {
+ ILocalReferenceOperation localReference => SymbolEqualityComparer.Default.Equals(localReference.Local, symbol),
+ IFieldReferenceOperation fieldReference => SymbolEqualityComparer.Default.Equals(fieldReference.Field, symbol),
+ IPropertyReferenceOperation propertyReference => SymbolEqualityComparer.Default.Equals(propertyReference.Property, symbol),
+ IParameterReferenceOperation parameterReference => SymbolEqualityComparer.Default.Equals(parameterReference.Parameter, symbol),
+ _ => false
+ };
+ }
+}
diff --git a/src/Paramore.Brighter.Analyzer/BrighterAnalyzerGlobals.cs b/src/Paramore.Brighter.Analyzer/BrighterAnalyzerGlobals.cs
index 4ad546154d..6c219b0749 100644
--- a/src/Paramore.Brighter.Analyzer/BrighterAnalyzerGlobals.cs
+++ b/src/Paramore.Brighter.Analyzer/BrighterAnalyzerGlobals.cs
@@ -1,4 +1,4 @@
-#region License
+#region License
/* The MIT License (MIT)
Copyright © 2026 Aboubakr Nasef
@@ -22,22 +22,33 @@ THE SOFTWARE. */
#endregion
-namespace Paramore.Brighter.Analyzer
+namespace Paramore.Brighter.Analyzer;
+
+public static class BrighterAnalyzerGlobals
{
- public class BrighterAnalyzerGlobals
- {
- public const string PublicationClassName = "Publication";
- public const string BrighterAssembly = "Paramore.Brighter";
- public const string RequestTypeProperty = "RequestType";
- public const string IRequestInterface = "IRequest";
-
- public const string MessagePumpTypeEnumName = "MessagePumpType";
- public const string SubscriptionClassName = "Subscription";
-
- public const string MessageMapperInterface = "IAmAMessageMapper";
- public const string UnwrapWithAttribute = "UnwrapWithAttribute";
- public const string WrapWithAttribute = "WrapWithAttribute";
- public const string MapToMessage = "MapToMessage";
- public const string MapToRequest = "MapToRequest";
- }
+ public const string PublicationClassName = "Publication";
+ public const string KafkaPublicationClassName = "KafkaPublication";
+ public const string BrighterAssembly = "Paramore.Brighter";
+ public const string KafkaNamespace = "Paramore.Brighter.MessagingGateway.Kafka";
+ public const string RequestTypeProperty = "RequestType";
+
+ // PartitionerProperty (the KafkaPublication property) and PartitionerEnum (the
+ // enum type) intentionally share the value "Partitioner" — they name different
+ // symbols and could diverge.
+ public const string PartitionerProperty = "Partitioner";
+ public const string PartitionerEnum = "Partitioner";
+ public const string ConsistentRandomPartitionerValue = "ConsistentRandom";
+ public const string ConsistentPartitionerValue = "Consistent";
+ public const string Murmur2RandomPartitionerValue = "Murmur2Random";
+ public const string Murmur2PartitionerValue = "Murmur2";
+ public const string IRequestInterface = "IRequest";
+
+ public const string MessagePumpTypeEnumName = "MessagePumpType";
+ public const string SubscriptionClassName = "Subscription";
+
+ public const string MessageMapperInterface = "IAmAMessageMapper";
+ public const string UnwrapWithAttribute = "UnwrapWithAttribute";
+ public const string WrapWithAttribute = "WrapWithAttribute";
+ public const string MapToMessage = "MapToMessage";
+ public const string MapToRequest = "MapToRequest";
}
diff --git a/src/Paramore.Brighter.Analyzer/DiagnosticsIds.cs b/src/Paramore.Brighter.Analyzer/DiagnosticsIds.cs
index 96e86e3c18..36237104c2 100644
--- a/src/Paramore.Brighter.Analyzer/DiagnosticsIds.cs
+++ b/src/Paramore.Brighter.Analyzer/DiagnosticsIds.cs
@@ -22,15 +22,16 @@ THE SOFTWARE. */
#endregion
+namespace Paramore.Brighter.Analyzer;
-namespace Paramore.Brighter.Analyzer
+public static class DiagnosticsIds
{
- public static class DiagnosticsIds
- {
- public const string RequestTypeMissing = "BRT001";
- public const string WrongRequestType = "BRT002";
- public const string MessagePumpMissing = "BRT003";
- public const string WrapWithAttribute = "BRT004";
- public const string UnWrapWithAttribute = "BRT005";
- }
+ public const string RequestTypeMissing = "BRT001";
+ public const string WrongRequestType = "BRT002";
+ public const string MessagePumpMissing = "BRT003";
+ public const string WrapWithAttribute = "BRT004";
+ public const string UnWrapWithAttribute = "BRT005";
+ public const string MissingPartitioner = "BRT006";
+ public const string ConsistentRandomPartitioner = "BRT007";
+ public const string ConsistentPartitioner = "BRT008";
}
diff --git a/src/Paramore.Brighter.Analyzer/Paramore.Brighter.Analyzer.csproj b/src/Paramore.Brighter.Analyzer/Paramore.Brighter.Analyzer.csproj
index 71264c5b3a..94588fea2e 100644
--- a/src/Paramore.Brighter.Analyzer/Paramore.Brighter.Analyzer.csproj
+++ b/src/Paramore.Brighter.Analyzer/Paramore.Brighter.Analyzer.csproj
@@ -1,4 +1,4 @@
-
+
$(BrighterNetStandardTargetFrameworks)
@@ -6,6 +6,7 @@
Analyzers for the brighter library
Analyzer;Scheduler;Message Scheduling;Command Processor;Brighter
false
+ enable
true
true
diff --git a/src/Paramore.Brighter.Analyzer/Visitors/Operation/KafkaPublicationPartitionerVisitor.cs b/src/Paramore.Brighter.Analyzer/Visitors/Operation/KafkaPublicationPartitionerVisitor.cs
new file mode 100644
index 0000000000..bcec77851c
--- /dev/null
+++ b/src/Paramore.Brighter.Analyzer/Visitors/Operation/KafkaPublicationPartitionerVisitor.cs
@@ -0,0 +1,118 @@
+#region License
+/* The MIT License (MIT)
+Copyright © 2026 Aboubakr Nasef
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the “Software”), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+
+#endregion
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Operations;
+
+namespace Paramore.Brighter.Analyzer.Visitors.Operation;
+
+public class KafkaPublicationPartitionerVisitor : OperationWalker
+{
+ private readonly INamedTypeSymbol _kafkaPublicationSymbol;
+ private readonly INamedTypeSymbol _partitionerEnumSymbol;
+
+ public KafkaPublicationPartitionerVisitor(INamedTypeSymbol kafkaPublicationSymbol, INamedTypeSymbol partitionerEnumSymbol)
+ {
+ _kafkaPublicationSymbol = kafkaPublicationSymbol;
+ _partitionerEnumSymbol = partitionerEnumSymbol;
+ }
+
+ public bool IsPartitionerAssigned { get; private set; }
+ public bool IsConsistentRandom { get; private set; }
+ public bool IsConsistent { get; private set; }
+ public string? PublicationName { get; private set; }
+ public Location? PartitionerAssignmentLocation { get; private set; }
+
+ public override void VisitObjectCreation(IObjectCreationOperation operation)
+ {
+ if (IsKafkaPublicationType(operation.Type, _kafkaPublicationSymbol))
+ {
+ PublicationName = operation.Type!.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
+
+ // base walks the children (including the initializer), which drives
+ // VisitSimpleAssignment for any Partitioner assignment. Only descend
+ // when this operation is the KafkaPublication itself; reporting for
+ // unrelated object creations would attribute nested publications to
+ // the wrong location. Note this also descends into nested object
+ // creations, so a nested KafkaPublication carrying its own Partitioner
+ // would mark the outer one as assigned too — a contrived edge case
+ // accepted for simplicity.
+ base.VisitObjectCreation(operation);
+ }
+ }
+
+ public override void VisitSimpleAssignment(ISimpleAssignmentOperation operation)
+ {
+ if (operation.Target is IPropertyReferenceOperation propertyReference &&
+ propertyReference.Property.Name == BrighterAnalyzerGlobals.PartitionerProperty &&
+ IsKafkaPublicationType(propertyReference.Property.ContainingType, _kafkaPublicationSymbol))
+ {
+ IsPartitionerAssigned = true;
+ PartitionerAssignmentLocation = operation.Syntax.GetLocation();
+
+ switch (GetPartitionerValueName(operation.Value, _partitionerEnumSymbol))
+ {
+ case BrighterAnalyzerGlobals.ConsistentRandomPartitionerValue:
+ IsConsistentRandom = true;
+ break;
+ case BrighterAnalyzerGlobals.ConsistentPartitionerValue:
+ IsConsistent = true;
+ break;
+ }
+ }
+
+ base.VisitSimpleAssignment(operation);
+ }
+
+ // Type can be null for erroneous code in the IDE; treat it as no match.
+ internal static bool IsKafkaPublicationType(ITypeSymbol? type, INamedTypeSymbol kafkaPublicationSymbol)
+ {
+ for (var current = type; current != null; current = current.BaseType)
+ {
+ if (SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, kafkaPublicationSymbol))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ internal static string? GetPartitionerValueName(IOperation value, INamedTypeSymbol partitionerEnumSymbol)
+ {
+ // Unwrap an implicit conversion (e.g. enum widening) if present.
+ if (value is IConversionOperation conversion)
+ {
+ value = conversion.Operand;
+ }
+
+ // Only fields of the Kafka Partitioner enum itself count; a user field
+ // that merely shares a member name (e.g. `Defaults.Consistent`) must not
+ // be treated as the enum value.
+ return value is IFieldReferenceOperation fieldReference &&
+ SymbolEqualityComparer.Default.Equals(fieldReference.Field.ContainingType, partitionerEnumSymbol)
+ ? fieldReference.Field.Name
+ : null;
+ }
+}
diff --git a/src/Paramore.Brighter.Analyzer/Visitors/Operation/RequestTypeAssignmentVisitor.cs b/src/Paramore.Brighter.Analyzer/Visitors/Operation/RequestTypeAssignmentVisitor.cs
index cc276c4ed1..e65583d6bd 100644
--- a/src/Paramore.Brighter.Analyzer/Visitors/Operation/RequestTypeAssignmentVisitor.cs
+++ b/src/Paramore.Brighter.Analyzer/Visitors/Operation/RequestTypeAssignmentVisitor.cs
@@ -1,4 +1,4 @@
-#region License
+#region License
/* The MIT License (MIT)
Copyright © 2026 Aboubakr Nasef
@@ -34,10 +34,10 @@ public class RequestTypeAssignmentVisitor : OperationWalker
{
public bool IsPublicationType { get; private set; }
public bool IsRequestTypeAssigned { get; private set; }
- public string PublicationName { get; private set; }
+ public string? PublicationName { get; private set; }
public bool IsNotTypeOfIRequest { get; private set; }
- public Location TypeOfLocation { get; private set; }
- public string TypeOfName { get; private set; }
+ public Location? TypeOfLocation { get; private set; }
+ public string? TypeOfName { get; private set; }
public override void VisitObjectCreation(IObjectCreationOperation operation)
{
diff --git a/src/Paramore.Brighter.Analyzer/Visitors/Operation/SubscriptionConstructorVisitor.cs b/src/Paramore.Brighter.Analyzer/Visitors/Operation/SubscriptionConstructorVisitor.cs
index 58fd98a830..31114fe9ab 100644
--- a/src/Paramore.Brighter.Analyzer/Visitors/Operation/SubscriptionConstructorVisitor.cs
+++ b/src/Paramore.Brighter.Analyzer/Visitors/Operation/SubscriptionConstructorVisitor.cs
@@ -1,4 +1,4 @@
-#region License
+#region License
/* The MIT License (MIT)
Copyright © 2026 Aboubakr Nasef
@@ -30,7 +30,7 @@ namespace Paramore.Brighter.Analyzer.Visitors.Operation
public class SubscriptionConstructorVisitor : OperationWalker
{
public bool IsMessagePumpDefault { get; private set; } = false;
- public string SubscriptionName { get; private set; }
+ public string? SubscriptionName { get; private set; }
public bool IsSubscriptionType { get; private set; }
public override void VisitObjectCreation(IObjectCreationOperation operation)
@@ -44,7 +44,7 @@ public override void VisitObjectCreation(IObjectCreationOperation operation)
}
public override void VisitArgument(IArgumentOperation operation)
{
- if (operation.Value.Type.Name == BrighterAnalyzerGlobals.MessagePumpTypeEnumName && operation.ArgumentKind == ArgumentKind.DefaultValue)
+ if (operation.Value.Type?.Name == BrighterAnalyzerGlobals.MessagePumpTypeEnumName && operation.ArgumentKind == ArgumentKind.DefaultValue)
{
IsMessagePumpDefault = true;
}
diff --git a/src/Paramore.Brighter.Analyzer/docs/BRT006.md b/src/Paramore.Brighter.Analyzer/docs/BRT006.md
new file mode 100644
index 0000000000..95096e6563
--- /dev/null
+++ b/src/Paramore.Brighter.Analyzer/docs/BRT006.md
@@ -0,0 +1,36 @@
+# BRT006: Missing Partitioner
+
+## Description
+This rule detects a `KafkaPublication` that is created without an explicit `Partitioner` assignment.
+
+## Why is this a warning?
+When the `Partitioner` is not set explicitly, the publication falls back to the default partitioning strategy — which is `Partitioner.ConsistentRandom`. Omitting the assignment therefore silently selects the same value that [BRT007](./BRT007.md) discourages, without that choice being visible in the code. Because the implicit case has the same partition-distribution impact as the explicit one, it is reported at the same **Warning** severity rather than as a lower-priority suggestion.
+
+The Brighter team wants users who work with Kafka to make the `Partitioner` choice explicit. The partitioner determines how message keys are distributed across a topic's partitions, and its impact — hash algorithm, keyless-message handling, and the risk of uneven load or *hot partitions* — is not common knowledge among many people who use Kafka. Setting it explicitly makes that decision visible in the code and prompts the author to understand the trade-offs (see [BRT007](./BRT007.md) and [BRT008](./BRT008.md)) rather than inheriting a default whose behaviour they may not be aware of.
+
+## How to fix
+Set the `Partitioner` explicitly on the `KafkaPublication`. `Partitioner.Murmur2Random` is the recommended value for new publications.
+
+**Changing the partitioner changes runtime behaviour.** Applying the fix to an existing publication moves it from the implicit `ConsistentRandom` default to `Murmur2Random`, which re-partitions the topic — keys will map to different partitions than before.
+
+**Keeping the current partition assignment.** No warning-free value preserves the existing key-to-partition mapping: the only value that matches the implicit default is `Partitioner.ConsistentRandom`, which [BRT007](./BRT007.md) flags — the warning-free values (`Murmur2Random`, `Murmur2`, `Random`) all re-partition the topic. To keep the current assignment while still making the choice visible in code, set `Partitioner = Partitioner.ConsistentRandom` explicitly and suppress BRT007 for that publication. Alternatively, leave it implicit and suppress BRT006.
+
+The rule recognizes a `Partitioner` assignment made in the object initializer, one made directly on the same instance (local, field, property or parameter) later in the same block — including inside nested blocks such as `if` statements — and one made by the constructor of a `KafkaPublication` subclass. This check is textual, not flow analysis: an assignment on a single conditional branch (`if (useKeys) publication.Partitioner = …`) or inside a loop counts as "set", even on paths where it never runs. Assignments made elsewhere — for example inside a helper method that configures the publication — are not tracked, so the warning may still fire for publications that are configured that way; suppress it in that case. Subclass constructors can only be inspected when the subclass is declared in source — a subclass from a referenced assembly that sets `Partitioner` in its constructor will still be flagged; suppress it there too.
+
+### Example
+```csharp
+// Warning: Partitioner assignment is missing
+var publication = new KafkaPublication
+{
+ // ... other properties
+};
+
+// Fixed: Partitioner assigned explicitly
+var publication = new KafkaPublication
+{
+ Partitioner = Partitioner.Murmur2Random
+ // ... other properties
+};
+```
+
+A code fix is available that adds `Partitioner = Partitioner.Murmur2Random` to the publication initializer.
diff --git a/src/Paramore.Brighter.Analyzer/docs/BRT007.md b/src/Paramore.Brighter.Analyzer/docs/BRT007.md
new file mode 100644
index 0000000000..cd999ccb99
--- /dev/null
+++ b/src/Paramore.Brighter.Analyzer/docs/BRT007.md
@@ -0,0 +1,29 @@
+# BRT007: ConsistentRandom Partitioner Used
+
+## Description
+This rule detects a `KafkaPublication` whose `Partitioner` is set to `Partitioner.ConsistentRandom`.
+
+## Why is this a warning?
+`Murmur2Random` is preferred over `ConsistentRandom` for new `KafkaPublications`. Both hash the message key to select a partition and spread keyless messages randomly, but they use different hash algorithms: `ConsistentRandom` uses CRC32, while `Murmur2Random` uses MurmurHash2 — the algorithm the standard Java Kafka producer uses. `Murmur2Random` is functionally equivalent to the Java producer's default partitioner, so a given key lands on the same partition for Brighter and for the standard Kafka clients. When other producers or consumers rely on key-based routing or ordering, that cross-client compatibility is what keeps a key on the partition the rest of the ecosystem expects.
+
+Existing publications that already rely on `ConsistentRandom` can safely ignore this warning to preserve their current partition assignment.
+
+## How to fix
+Change the `Partitioner` from `Partitioner.ConsistentRandom` to `Partitioner.Murmur2Random`.
+
+### Example
+```csharp
+// Warning: ConsistentRandom used
+var publication = new KafkaPublication
+{
+ Partitioner = Partitioner.ConsistentRandom
+};
+
+// Fixed: prefer Murmur2Random
+var publication = new KafkaPublication
+{
+ Partitioner = Partitioner.Murmur2Random
+};
+```
+
+A code fix is available that replaces `Partitioner.ConsistentRandom` with `Partitioner.Murmur2Random`.
diff --git a/src/Paramore.Brighter.Analyzer/docs/BRT008.md b/src/Paramore.Brighter.Analyzer/docs/BRT008.md
new file mode 100644
index 0000000000..69d39d1e56
--- /dev/null
+++ b/src/Paramore.Brighter.Analyzer/docs/BRT008.md
@@ -0,0 +1,33 @@
+# BRT008: Consistent Partitioner Used
+
+## Description
+This rule detects a `KafkaPublication` whose `Partitioner` is set to `Partitioner.Consistent`.
+
+## Why is this a warning?
+`Murmur2` is preferred over `Consistent` for new `KafkaPublications`. Both hash the message key to select a partition, but `Murmur2` uses the MurmurHash2 algorithm, which spreads keys more evenly across partitions than the CRC32-based hash used by `Consistent`. Murmur2 is also the algorithm the standard Java Kafka producer uses for keyed messages, so a given key lands on the same partition for Brighter and for the standard Kafka clients — which matters when other producers or consumers rely on key-based routing or ordering.
+
+An uneven hash concentrates a disproportionate share of keys onto a few partitions — the *hot partition* problem. Because each partition is served by a single consumer within a consumer group and a single broker as its leader, a hot partition becomes a throughput bottleneck: it lags and backs up while the remaining partitions sit under-used, so the topic can no longer scale across all of its partitions. `Murmur2`'s more uniform distribution keeps load balanced and avoids this.
+
+Like `Consistent`, `Murmur2` pins messages with empty or NULL keys to a single partition. If the publication sends keyless messages, prefer `Partitioner.Murmur2Random` (see [BRT007](./BRT007.md)) instead, which spreads them across partitions.
+
+Existing publications that already rely on `Consistent` can safely ignore this warning to preserve their current partition assignment.
+
+## How to fix
+Change the `Partitioner` from `Partitioner.Consistent` to `Partitioner.Murmur2`.
+
+### Example
+```csharp
+// Warning: Consistent used
+var publication = new KafkaPublication
+{
+ Partitioner = Partitioner.Consistent
+};
+
+// Fixed: prefer Murmur2
+var publication = new KafkaPublication
+{
+ Partitioner = Partitioner.Murmur2
+};
+```
+
+A code fix is available that replaces `Partitioner.Consistent` with `Partitioner.Murmur2`.
diff --git a/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/BaseAnalyzerTest.cs b/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/BaseAnalyzerTest.cs
index 7f3e4da868..dfdc8353e0 100644
--- a/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/BaseAnalyzerTest.cs
+++ b/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/BaseAnalyzerTest.cs
@@ -1,24 +1,28 @@
-
-using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Testing;
using Paramore.Brighter.Analyzer.Analyzers;
-namespace Paramore.Brighter.Analyzer.Tests.Analyzers
+namespace Paramore.Brighter.Analyzer.Tests.Analyzers;
+
+public abstract class BaseAnalyzerTest
+ where T : DiagnosticAnalyzer, new()
{
- public abstract class BaseAnalyzerTest where T : DiagnosticAnalyzer, new()
+ protected CSharpAnalyzerTest testContext;
+
+ protected BaseAnalyzerTest()
{
- protected CSharpAnalyzerTest testContext;
- protected BaseAnalyzerTest()
+ testContext = new CSharpAnalyzerTest
{
- testContext = new CSharpAnalyzerTest
- {
- ReferenceAssemblies = ReferenceAssemblies.Net.Net90
- };
- testContext.TestState.OutputKind = OutputKind.ConsoleApplication;
- testContext.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Publication).Assembly.Location));
- testContext.CompilerDiagnostics = CompilerDiagnostics.None;
- }
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net90,
+ };
+
+ testContext.TestState.OutputKind = OutputKind.ConsoleApplication;
+ testContext.TestState.AdditionalReferences.Add(
+ MetadataReference.CreateFromFile(typeof(Publication).Assembly.Location)
+ );
+
+ testContext.CompilerDiagnostics = CompilerDiagnostics.None;
}
}
diff --git a/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/BaseKafkaAnalyzerTest.cs b/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/BaseKafkaAnalyzerTest.cs
new file mode 100644
index 0000000000..3a7eb059b7
--- /dev/null
+++ b/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/BaseKafkaAnalyzerTest.cs
@@ -0,0 +1,19 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+using Paramore.Brighter.Analyzer.Analyzers;
+
+namespace Paramore.Brighter.Analyzer.Tests.Analyzers;
+
+public abstract class BaseKafkaAnalyzerTest : BaseAnalyzerTest
+{
+ protected BaseKafkaAnalyzerTest()
+ {
+ testContext.TestState.OutputKind = OutputKind.DynamicallyLinkedLibrary;
+ testContext.TestState.AdditionalReferences.Add(
+ MetadataReference.CreateFromFile(
+ typeof(Paramore.Brighter.MessagingGateway.Kafka.KafkaPublication).Assembly.Location
+ )
+ );
+ testContext.CompilerDiagnostics = CompilerDiagnostics.Errors;
+ }
+}
diff --git a/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/KafkaPublicationPartitionerAnalyzerTest.cs b/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/KafkaPublicationPartitionerAnalyzerTest.cs
new file mode 100644
index 0000000000..fb8ae5b81b
--- /dev/null
+++ b/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/KafkaPublicationPartitionerAnalyzerTest.cs
@@ -0,0 +1,798 @@
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Testing;
+using Paramore.Brighter.Analyzer.Analyzers;
+using Xunit;
+
+namespace Paramore.Brighter.Analyzer.Tests.Analyzers;
+
+public class KafkaPublicationPartitionerAnalyzerTest : BaseKafkaAnalyzerTest
+{
+ [Fact]
+ public async Task When_KafkaPublication_Is_Created_Without_Partitioner_Should_Report_Missing_Partitioner()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new {|#0:KafkaPublication|}();
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_KafkaPublication_Generic_Is_Created_Without_Partitioner_Should_Report_Missing_Partitioner()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class MyRequest : IRequest
+ {
+ public Id Id { get; set; }
+ public Id? CorrelationId { get; set; }
+ }
+
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new {|#0:KafkaPublication|}();
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_KafkaPublication_Is_Created_With_ConsistentRandom_Should_Report_Warning()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ {|#0:Partitioner = Partitioner.ConsistentRandom|}
+ };
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentRandomPartitionerRule
+ ).WithLocation(0)
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_KafkaPublication_Is_Created_With_Consistent_Should_Report_Warning()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ {|#0:Partitioner = Partitioner.Consistent|}
+ };
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentPartitionerRule
+ ).WithLocation(0)
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_KafkaPublication_Is_Created_With_Murmur2Random_Should_Not_Report()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ Partitioner = Partitioner.Murmur2Random
+ };
+ }
+ }
+}
+""";
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_KafkaPublication_Without_Partitioner_Is_Nested_In_Another_Object_Creation_Should_Report_Once_At_Publication()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class Holder
+ {
+ public Holder(KafkaPublication publication) { }
+ }
+
+ class TypeName
+ {
+ public void Method()
+ {
+ var holder = new Holder(new {|#0:KafkaPublication|}());
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_KafkaPublication_With_Consistent_Is_Nested_In_Another_Object_Creation_Should_Report_Once_At_Publication()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class Holder
+ {
+ public Holder(KafkaPublication publication) { }
+ }
+
+ class TypeName
+ {
+ public void Method()
+ {
+ var holder = new Holder(new KafkaPublication
+ {
+ {|#0:Partitioner = Partitioner.Consistent|}
+ });
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentPartitionerRule
+ ).WithLocation(0)
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_KafkaPublication_Is_Created_With_Random_Should_Not_Report()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ Partitioner = Partitioner.Random
+ };
+ }
+ }
+}
+""";
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Nested_Object_Has_Own_Partitioner_Property_Should_Still_Report_Missing_Partitioner()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class Config
+ {
+ public int Partitioner { get; set; }
+ }
+
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new {|#0:KafkaPublication|}
+ {
+ DefaultHeaders = new System.Collections.Generic.Dictionary
+ {
+ ["key"] = new Config { Partitioner = 3 }
+ }
+ };
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Partitioner_Is_Set_After_Construction_Should_Not_Report()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication();
+ publication.Partitioner = Partitioner.Murmur2Random;
+ }
+ }
+}
+""";
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Consistent_Is_Set_After_Construction_Should_Report_Warning_At_Assignment()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication();
+ {|#0:publication.Partitioner = Partitioner.Consistent|};
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentPartitionerRule
+ ).WithLocation(0)
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_ConsistentRandom_Is_Set_After_Construction_Should_Report_Warning_At_Assignment()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication();
+ {|#0:publication.Partitioner = Partitioner.ConsistentRandom|};
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentRandomPartitionerRule
+ ).WithLocation(0)
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Plain_Publication_Is_Created_Should_Not_Report()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new Publication();
+ }
+ }
+}
+""";
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_KafkaPublication_Subclass_Is_Created_Without_Partitioner_Should_Report_Missing_Partitioner()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class MyPublication : KafkaPublication
+ {
+ }
+
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new {|#0:MyPublication|}();
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("MyPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Partitioner_Is_Set_On_Field_After_Construction_Should_Not_Report()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ private KafkaPublication _publication;
+
+ public void Method()
+ {
+ _publication = new KafkaPublication();
+ _publication.Partitioner = Partitioner.Murmur2Random;
+ }
+ }
+}
+""";
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Partitioner_Is_Set_Before_Construction_Should_Still_Report_Missing_Partitioner()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ private KafkaPublication _publication;
+
+ public void Method()
+ {
+ _publication.Partitioner = Partitioner.Murmur2Random;
+ _publication = new {|#0:KafkaPublication|}();
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_KafkaPublication_Is_Created_With_Target_Typed_New_Without_Partitioner_Should_Report_Missing_Partitioner()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ KafkaPublication publication = {|#0:new|}();
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_KafkaPublication_Generic_Is_Created_With_ConsistentRandom_Should_Report_Warning()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class MyRequest : IRequest
+ {
+ public Id Id { get; set; }
+ public Id? CorrelationId { get; set; }
+ }
+
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ {|#0:Partitioner = Partitioner.ConsistentRandom|}
+ };
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentRandomPartitionerRule
+ ).WithLocation(0)
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Subclass_Sets_Partitioner_In_Constructor_Should_Not_Report()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class OrdersPublication : KafkaPublication
+ {
+ public OrdersPublication()
+ {
+ Partitioner = Partitioner.Murmur2Random;
+ }
+ }
+
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new OrdersPublication();
+ }
+ }
+}
+""";
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Subclass_Sets_Consistent_In_Constructor_Should_Report_Warning_At_Assignment()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class OrdersPublication : KafkaPublication
+ {
+ public OrdersPublication()
+ {
+ {|#0:Partitioner = Partitioner.Consistent|};
+ }
+ }
+
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new OrdersPublication();
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentPartitionerRule
+ ).WithLocation(0)
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Value_Is_A_User_Field_Named_Like_The_Enum_Member_Should_Not_Report()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ static class Defaults
+ {
+ public static readonly Partitioner Consistent = Partitioner.Murmur2;
+ }
+
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ Partitioner = Defaults.Consistent
+ };
+ }
+ }
+}
+""";
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Consistent_Is_Set_In_Nested_Member_Initializer_Should_Report_Warning_At_Assignment()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class Holder
+ {
+ public KafkaPublication Publication { get; set; } = new KafkaPublication { Partitioner = Partitioner.Murmur2Random };
+ }
+
+ class TypeName
+ {
+ public void Method()
+ {
+ var holder = new Holder
+ {
+ Publication = { {|#0:Partitioner = Partitioner.Consistent|} }
+ };
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentPartitionerRule
+ ).WithLocation(0)
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Partitioner_Is_Set_On_Parameter_After_Construction_Should_Not_Report()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method(KafkaPublication publication)
+ {
+ publication = new KafkaPublication();
+ publication.Partitioner = Partitioner.Murmur2Random;
+ }
+ }
+}
+""";
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Partitioner_Is_Set_On_Property_After_Construction_Should_Not_Report()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ private KafkaPublication Publication { get; set; }
+
+ public void Method()
+ {
+ Publication = new KafkaPublication();
+ Publication.Partitioner = Partitioner.Murmur2Random;
+ }
+ }
+}
+""";
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Target_Typed_New_Is_Created_With_Consistent_Should_Report_Warning()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ KafkaPublication publication = new()
+ {
+ {|#0:Partitioner = Partitioner.Consistent|}
+ };
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentPartitionerRule
+ ).WithLocation(0)
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Consistent_Is_Set_In_Nested_Block_Should_Report_Only_Warning_At_Assignment()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method(bool legacy)
+ {
+ var publication = new KafkaPublication();
+ if (legacy)
+ {
+ {|#0:publication.Partitioner = Partitioner.Consistent|};
+ }
+ }
+ }
+}
+""";
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentPartitionerRule
+ ).WithLocation(0)
+ );
+
+ await testContext.RunAsync();
+ }
+}
diff --git a/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/PublicationRequestTypeAssignmentAnalyzerTest.cs b/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/PublicationRequestTypeAssignmentAnalyzerTest.cs
index a8e904138a..978f26bfed 100644
--- a/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/PublicationRequestTypeAssignmentAnalyzerTest.cs
+++ b/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/PublicationRequestTypeAssignmentAnalyzerTest.cs
@@ -2,17 +2,16 @@
using Paramore.Brighter.Analyzer.Analyzers;
using Paramore.Brighter.Analyzer.Tests.Analyzers;
+namespace Paramore.Brighter.Analyzer.Test.Analyzers;
-namespace Paramore.Brighter.Analyzer.Test.Analyzers
+public class PublicationRequestTypeAssignmentAnalyzerTest
+ : BaseAnalyzerTest
{
- public class PublicationRequestTypeAssignmentAnalyzerTest : BaseAnalyzerTest
+ [Fact]
+ public async Task When_Initializing_Publication_WithOut_RequestType()
{
-
- [Fact]
- public async Task When_Initializing_Publication_WithOut_RequestType()
- {
-
- testContext.TestCode = /* lang=c#-test */ """
+ testContext.TestCode = /* lang=c#-test */
+ """
using Paramore.Brighter;
namespace TestNamespace
{
@@ -24,14 +23,19 @@ public class PublicationTest : Publication
}
""";
- testContext.ExpectedDiagnostics.Add(new DiagnosticResult(PublicationRequestTypeAssignmentAnalyzer.RequestTypeMissingRule).WithLocation(0).WithArguments("PublicationTest"));
- await testContext.RunAsync();
- }
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(PublicationRequestTypeAssignmentAnalyzer.RequestTypeMissingRule)
+ .WithLocation(0)
+ .WithArguments("PublicationTest")
+ );
+ await testContext.RunAsync();
+ }
- [Fact]
- public async Task When_Initializing_Publication_With_Right_RequestType()
- {
- testContext.TestCode = /* lang=c#-test */ """
+ [Fact]
+ public async Task When_Initializing_Publication_With_Right_RequestType()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
using Paramore.Brighter;
namespace TestNamespace
{
@@ -51,13 +55,14 @@ public EventSample(Id id) : base(id)
}
}
""";
- await testContext.RunAsync();
- }
+ await testContext.RunAsync();
+ }
- [Fact]
- public async Task When_Initializing_Publication_With_Wrong_RequestType()
- {
- testContext.TestCode = /* lang=c#-test */ """
+ [Fact]
+ public async Task When_Initializing_Publication_With_Wrong_RequestType()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
using Paramore.Brighter;
namespace TestNamespace
{
@@ -71,15 +76,20 @@ public class PublicationTest : Publication
public class EventSample{}
}
""";
- testContext.ExpectedDiagnostics.Add(new DiagnosticResult(PublicationRequestTypeAssignmentAnalyzer.WrongRequestTypeRule).WithLocation(0).WithArguments("EventSample"));
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(PublicationRequestTypeAssignmentAnalyzer.WrongRequestTypeRule)
+ .WithLocation(0)
+ .WithArguments("EventSample")
+ );
- await testContext.RunAsync();
- }
+ await testContext.RunAsync();
+ }
- [Fact]
- public async Task When_Initializing_Non_Publication_Type()
- {
- testContext.TestCode = /* lang=c#-test */ """
+ [Fact]
+ public async Task When_Initializing_Non_Publication_Type()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
using System.Collections.Generic;
namespace TestNamespace
{
@@ -92,7 +102,6 @@ public void Method()
}
}
""";
- await testContext.RunAsync();
- }
+ await testContext.RunAsync();
}
}
diff --git a/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/SubscriptionConstructorAnalyzerTest.cs b/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/SubscriptionConstructorAnalyzerTest.cs
index 8d2bf4d8a2..4fdab08f56 100644
--- a/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/SubscriptionConstructorAnalyzerTest.cs
+++ b/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/SubscriptionConstructorAnalyzerTest.cs
@@ -2,15 +2,15 @@
using Paramore.Brighter.Analyzer.Analyzers;
using Paramore.Brighter.Analyzer.Tests.Analyzers;
-namespace Paramore.Brighter.Analyzer.Test.Analyzers
+namespace Paramore.Brighter.Analyzer.Test.Analyzers;
+
+public class SubscriptionConstructorAnalyzerTest : BaseAnalyzerTest
{
- public class SubscriptionConstructorAnalyzerTest : BaseAnalyzerTest
+ [Fact]
+ public async Task When_Initializing_Subscription_With_MessagePump()
{
- [Fact]
- public async Task When_Initializing_Subscription_With_MessagePump()
- {
-
- testContext.TestCode = /* lang=c#-test */ """
+ testContext.TestCode = /* lang=c#-test */
+ """
using Paramore.Brighter;
namespace TestNamespace
{
@@ -25,14 +25,14 @@ public SubscriptionTest(SubscriptionName subscriptionName, ChannelName channelNa
}
""";
- await testContext.RunAsync();
- }
-
- [Fact]
- public async Task When_Initializing_Subscription_WithOut_MessagePump()
- {
+ await testContext.RunAsync();
+ }
- testContext.TestCode = /* lang=c#-test */ """
+ [Fact]
+ public async Task When_Initializing_Subscription_WithOut_MessagePump()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
using Paramore.Brighter;
namespace TestNamespace
{
@@ -47,14 +47,19 @@ public SubscriptionTest(SubscriptionName subscriptionName, ChannelName channelNa
}
""";
- testContext.ExpectedDiagnostics.Add(new DiagnosticResult(SubscriptionConstructorAnalyzer.MessagePumpMissingRule).WithLocation(0).WithArguments("SubscriptionTest"));
- await testContext.RunAsync();
- }
- [Fact]
- public async Task When_Initializing_SubscriptionNested_WithOut_MessagePump()
- {
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(SubscriptionConstructorAnalyzer.MessagePumpMissingRule)
+ .WithLocation(0)
+ .WithArguments("SubscriptionTest")
+ );
+ await testContext.RunAsync();
+ }
- testContext.TestCode = /* lang=c#-test */ """
+ [Fact]
+ public async Task When_Initializing_SubscriptionNested_WithOut_MessagePump()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
using Paramore.Brighter;
namespace TestNamespace
{
@@ -75,9 +80,11 @@ public SubscriptionTestNested(SubscriptionName subscriptionName, ChannelName cha
}
""";
- testContext.ExpectedDiagnostics.Add(new DiagnosticResult(SubscriptionConstructorAnalyzer.MessagePumpMissingRule).WithLocation(0).WithArguments("SubscriptionTestNested"));
- await testContext.RunAsync();
- }
-
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(SubscriptionConstructorAnalyzer.MessagePumpMissingRule)
+ .WithLocation(0)
+ .WithArguments("SubscriptionTestNested")
+ );
+ await testContext.RunAsync();
}
}
diff --git a/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/WrapAttributeAnalyzerTest.cs b/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/WrapAttributeAnalyzerTest.cs
index 8ad50ccffd..a212215c7e 100644
--- a/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/WrapAttributeAnalyzerTest.cs
+++ b/tests/Paramore.Brighter.Analyzer.Tests/Analyzers/WrapAttributeAnalyzerTest.cs
@@ -2,16 +2,15 @@
using Paramore.Brighter.Analyzer.Analyzers;
using Paramore.Brighter.Analyzer.Tests.Analyzers;
-namespace Paramore.Brighter.Analyzer.Test.Analyzers
+namespace Paramore.Brighter.Analyzer.Test.Analyzers;
+
+public class WrapAttributeAnalyzerTest : BaseAnalyzerTest
{
- public class WrapAttributeAnalyzerTest: BaseAnalyzerTest
+ [Fact]
+ public async Task When_Adding_Attribute_To_MessageMapper()
{
-
- [Fact]
- public async Task When_Adding_Attribute_To_MessageMapper()
- {
-
- testContext.TestCode = /* lang=c#-test */ """
+ testContext.TestCode = /* lang=c#-test */
+ """
using Paramore.Brighter;
using Paramore.Brighter.Transforms.Attributes;
namespace TestNamespace
@@ -38,9 +37,16 @@ public class SampleEvent(Id id) : Event(id)
}
""";
- testContext.ExpectedDiagnostics.Add(new DiagnosticResult(WrapAttributeAnalyzer.WrapAttributeRule ).WithLocation(0).WithArguments("CompressAttribute"));
- testContext.ExpectedDiagnostics.Add(new DiagnosticResult(WrapAttributeAnalyzer.UnWrapWithAttributeRule ).WithLocation(1).WithArguments("DecompressAttribute"));
- await testContext.RunAsync();
- }
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(WrapAttributeAnalyzer.WrapAttributeRule)
+ .WithLocation(0)
+ .WithArguments("CompressAttribute")
+ );
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(WrapAttributeAnalyzer.UnWrapWithAttributeRule)
+ .WithLocation(1)
+ .WithArguments("DecompressAttribute")
+ );
+ await testContext.RunAsync();
}
}
diff --git a/tests/Paramore.Brighter.Analyzer.Tests/CodeFixes/BaseCodeFixTest.cs b/tests/Paramore.Brighter.Analyzer.Tests/CodeFixes/BaseCodeFixTest.cs
new file mode 100644
index 0000000000..0fcd53bfc7
--- /dev/null
+++ b/tests/Paramore.Brighter.Analyzer.Tests/CodeFixes/BaseCodeFixTest.cs
@@ -0,0 +1,35 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Testing;
+
+namespace Paramore.Brighter.Analyzer.Tests.CodeFixes;
+
+public abstract class BaseCodeFixTest
+ where TAnalyzer : DiagnosticAnalyzer, new()
+ where TCodeFix : CodeFixProvider, new()
+{
+ protected CSharpCodeFixTest testContext;
+
+ protected BaseCodeFixTest()
+ {
+ testContext = new CSharpCodeFixTest
+ {
+ ReferenceAssemblies = ReferenceAssemblies.Net.Net90,
+ };
+
+ testContext.TestState.OutputKind = OutputKind.DynamicallyLinkedLibrary;
+ testContext.TestState.AdditionalReferences.Add(
+ MetadataReference.CreateFromFile(typeof(Publication).Assembly.Location)
+ );
+
+ testContext.TestState.AdditionalReferences.Add(
+ MetadataReference.CreateFromFile(
+ typeof(Paramore.Brighter.MessagingGateway.Kafka.KafkaPublication).Assembly.Location
+ )
+ );
+
+ testContext.CompilerDiagnostics = CompilerDiagnostics.Errors;
+ }
+}
diff --git a/tests/Paramore.Brighter.Analyzer.Tests/CodeFixes/MissingPartitionerCodeFixProviderTest.cs b/tests/Paramore.Brighter.Analyzer.Tests/CodeFixes/MissingPartitionerCodeFixProviderTest.cs
new file mode 100644
index 0000000000..171647e95f
--- /dev/null
+++ b/tests/Paramore.Brighter.Analyzer.Tests/CodeFixes/MissingPartitionerCodeFixProviderTest.cs
@@ -0,0 +1,634 @@
+using System.IO;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Testing;
+using Paramore.Brighter.Analyzer.Analyzers;
+using Paramore.Brighter.Analyzer.CodeFixes;
+using Xunit;
+
+namespace Paramore.Brighter.Analyzer.Tests.CodeFixes;
+
+public class MissingPartitionerCodeFixProviderTest
+ : BaseCodeFixTest
+{
+ [Fact]
+ public async Task When_Partitioner_Is_Missing_Should_Add_Murmur2Random()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new {|#0:KafkaPublication|}();
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication { Partitioner = Partitioner.Murmur2Random };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Kafka_Using_Is_Missing_Should_Add_Fully_Qualified_Partitioner()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new {|#0:Paramore.Brighter.MessagingGateway.Kafka.KafkaPublication|}();
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new Paramore.Brighter.MessagingGateway.Kafka.KafkaPublication { Partitioner = Paramore.Brighter.MessagingGateway.Kafka.Partitioner.Murmur2Random };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Partitioner_Is_Missing_Should_Append_To_Existing_Initializer()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new {|#0:KafkaPublication|}
+ {
+ Topic = new RoutingKey("x"),
+ NumPartitions = 3
+ };
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ Topic = new RoutingKey("x"),
+ NumPartitions = 3,
+ Partitioner = Partitioner.Murmur2Random
+ };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Partitioner_Is_Missing_Should_Append_After_Trailing_Comment()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new {|#0:KafkaPublication|}
+ {
+ Topic = new RoutingKey("x"),
+ NumPartitions = 3 // one per shard
+ };
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ Topic = new RoutingKey("x"),
+ NumPartitions = 3, // one per shard
+ Partitioner = Partitioner.Murmur2Random
+ };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Partitioner_Is_Missing_On_Target_Typed_New_Should_Add_Murmur2Random()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ KafkaPublication publication = {|#0:new|}();
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ KafkaPublication publication = new()
+ {
+ Partitioner = Partitioner.Murmur2Random
+ };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Partitioner_Is_Missing_On_Single_Line_Initializer_Should_Append_On_Same_Line()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new {|#0:KafkaPublication|} { Topic = new RoutingKey("x") };
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication { Topic = new RoutingKey("x"), Partitioner = Partitioner.Murmur2Random };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task FixAll_Should_Add_Partitioner_To_All_Publications()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var first = new {|#0:KafkaPublication|}();
+ var second = new {|#1:KafkaPublication|}();
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var first = new KafkaPublication { Partitioner = Partitioner.Murmur2Random };
+ var second = new KafkaPublication { Partitioner = Partitioner.Murmur2Random };
+ }
+ }
+}
+""";
+
+ testContext.BatchFixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var first = new KafkaPublication { Partitioner = Partitioner.Murmur2Random };
+ var second = new KafkaPublication { Partitioner = Partitioner.Murmur2Random };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(1)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Partitioner_Is_Missing_On_Empty_Initializer_Should_Add_Murmur2Random()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new {|#0:KafkaPublication|} { };
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication { Partitioner = Partitioner.Murmur2Random };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Partitioner_Is_Missing_Should_Append_After_Trailing_Comma()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new {|#0:KafkaPublication|}
+ {
+ Topic = new RoutingKey("x"),
+ NumPartitions = 3,
+ };
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ Topic = new RoutingKey("x"),
+ NumPartitions = 3,
+ Partitioner = Partitioner.Murmur2Random
+ };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Partitioner_Is_Missing_Should_Append_After_Single_Expression_Trailing_Comma()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new {|#0:KafkaPublication|} { Topic = new RoutingKey("x"), };
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication { Topic = new RoutingKey("x"), Partitioner = Partitioner.Murmur2Random };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Partitioner_Is_Missing_On_Generic_Should_Add_Murmur2Random()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class MyRequest : IRequest
+ {
+ public Id Id { get; set; }
+ public Id? CorrelationId { get; set; }
+ }
+
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new {|#0:KafkaPublication|}();
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class MyRequest : IRequest
+ {
+ public Id Id { get; set; }
+ public Id? CorrelationId { get; set; }
+ }
+
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication { Partitioner = Partitioner.Murmur2Random };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Partitioner_Is_Ambiguous_With_Confluent_Should_Add_Fully_Qualified_Partitioner()
+ {
+ testContext.TestState.AdditionalReferences.Add(
+ MetadataReference.CreateFromFile(
+ Path.Combine(
+ Path.GetDirectoryName(typeof(Paramore.Brighter.MessagingGateway.Kafka.KafkaPublication).Assembly.Location),
+ "Confluent.Kafka.dll")));
+
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+using Confluent.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new {|#0:KafkaPublication|}();
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+using Confluent.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication { Partitioner = Paramore.Brighter.MessagingGateway.Kafka.Partitioner.Murmur2Random };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(KafkaPublicationPartitionerAnalyzer.MissingPartitionerRule)
+ .WithLocation(0)
+ .WithArguments("KafkaPublication")
+ );
+
+ await testContext.RunAsync();
+ }
+}
diff --git a/tests/Paramore.Brighter.Analyzer.Tests/CodeFixes/PartitionerValueCodeFixProviderTest.cs b/tests/Paramore.Brighter.Analyzer.Tests/CodeFixes/PartitionerValueCodeFixProviderTest.cs
new file mode 100644
index 0000000000..9dbb7d7db9
--- /dev/null
+++ b/tests/Paramore.Brighter.Analyzer.Tests/CodeFixes/PartitionerValueCodeFixProviderTest.cs
@@ -0,0 +1,390 @@
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis.Testing;
+using Paramore.Brighter.Analyzer.Analyzers;
+using Paramore.Brighter.Analyzer.CodeFixes;
+using Xunit;
+
+namespace Paramore.Brighter.Analyzer.Tests.CodeFixes;
+
+public class PartitionerValueCodeFixProviderTest
+ : BaseCodeFixTest
+{
+ [Fact]
+ public async Task When_ConsistentRandom_Is_Used_Should_Offer_Murmur2Random()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ {|#0:Partitioner = Partitioner.ConsistentRandom|}
+ };
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ Partitioner = Partitioner.Murmur2Random
+ };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentRandomPartitionerRule
+ ).WithLocation(0)
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Consistent_Is_Used_Should_Offer_Murmur2()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ {|#0:Partitioner = Partitioner.Consistent|}
+ };
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ Partitioner = Partitioner.Murmur2
+ };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentPartitionerRule
+ ).WithLocation(0)
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Bare_Identifier_Via_Using_Static_Is_Used_Should_Offer_Murmur2Random()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+using static Paramore.Brighter.MessagingGateway.Kafka.Partitioner;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ {|#0:Partitioner = ConsistentRandom|}
+ };
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+using static Paramore.Brighter.MessagingGateway.Kafka.Partitioner;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ Partitioner = Murmur2Random
+ };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentRandomPartitionerRule
+ ).WithLocation(0)
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Consistent_Is_Set_After_Construction_Should_Offer_Murmur2()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication();
+ {|#0:publication.Partitioner = Partitioner.Consistent|};
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication();
+ publication.Partitioner = Partitioner.Murmur2;
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentPartitionerRule
+ ).WithLocation(0)
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Value_Is_Parenthesized_Should_Offer_Murmur2()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ {|#0:Partitioner = (Partitioner.Consistent)|}
+ };
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ Partitioner = (Partitioner.Murmur2)
+ };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentPartitionerRule
+ ).WithLocation(0)
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task When_Value_Is_A_Cast_Should_Not_Offer_Fix()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var publication = new KafkaPublication
+ {
+ {|#0:Partitioner = (Partitioner)Partitioner.Consistent|}
+ };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentPartitionerRule
+ ).WithLocation(0)
+ );
+
+ await testContext.RunAsync();
+ }
+
+ [Fact]
+ public async Task FixAll_Should_Replace_All_Discouraged_Values()
+ {
+ testContext.TestCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var first = new KafkaPublication
+ {
+ {|#0:Partitioner = Partitioner.Consistent|}
+ };
+ var second = new KafkaPublication
+ {
+ {|#1:Partitioner = Partitioner.Consistent|}
+ };
+ }
+ }
+}
+""";
+
+ testContext.FixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var first = new KafkaPublication
+ {
+ Partitioner = Partitioner.Murmur2
+ };
+ var second = new KafkaPublication
+ {
+ Partitioner = Partitioner.Murmur2
+ };
+ }
+ }
+}
+""";
+
+ testContext.BatchFixedCode = /* lang=c#-test */
+ """
+using Paramore.Brighter;
+using Paramore.Brighter.MessagingGateway.Kafka;
+
+namespace ConsoleApplication1
+{
+ class TypeName
+ {
+ public void Method()
+ {
+ var first = new KafkaPublication
+ {
+ Partitioner = Partitioner.Murmur2
+ };
+ var second = new KafkaPublication
+ {
+ Partitioner = Partitioner.Murmur2
+ };
+ }
+ }
+}
+""";
+
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentPartitionerRule
+ ).WithLocation(0)
+ );
+ testContext.ExpectedDiagnostics.Add(
+ new DiagnosticResult(
+ KafkaPublicationPartitionerAnalyzer.ConsistentPartitionerRule
+ ).WithLocation(1)
+ );
+
+ await testContext.RunAsync();
+ }
+}
diff --git a/tests/Paramore.Brighter.Analyzer.Tests/Paramore.Brighter.Analyzer.Tests.csproj b/tests/Paramore.Brighter.Analyzer.Tests/Paramore.Brighter.Analyzer.Tests.csproj
index 1eef7fe76a..fcb1e9c9d3 100644
--- a/tests/Paramore.Brighter.Analyzer.Tests/Paramore.Brighter.Analyzer.Tests.csproj
+++ b/tests/Paramore.Brighter.Analyzer.Tests/Paramore.Brighter.Analyzer.Tests.csproj
@@ -14,12 +14,15 @@
+
+
+