diff --git a/all.sln b/all.sln
index 337ac2f35..01a31fc0a 100644
--- a/all.sln
+++ b/all.sln
@@ -159,6 +159,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Analyzers", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Workflow.Analyzers.Test", "test\Dapr.Workflow.Analyzers.Test\Dapr.Workflow.Analyzers.Test.csproj", "{CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Pubsub.Analyzers", "src\Dapr.Pubsub.Analyzers\Dapr.Pubsub.Analyzers.csproj", "{984BBCC6-C827-430A-8796-C4EB55E7D979}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Pubsub.Analyzers.Test", "test\Dapr.Pubsub.Analyzers.Test\Dapr.Pubsub.Analyzers.Test.csproj", "{A716C2AB-1B38-4284-AC94-2C3D9B758534}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -415,6 +419,14 @@ Global
{CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539}.Release|Any CPU.Build.0 = Release|Any CPU
+ {984BBCC6-C827-430A-8796-C4EB55E7D979}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {984BBCC6-C827-430A-8796-C4EB55E7D979}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {984BBCC6-C827-430A-8796-C4EB55E7D979}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {984BBCC6-C827-430A-8796-C4EB55E7D979}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A716C2AB-1B38-4284-AC94-2C3D9B758534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A716C2AB-1B38-4284-AC94-2C3D9B758534}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A716C2AB-1B38-4284-AC94-2C3D9B758534}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A716C2AB-1B38-4284-AC94-2C3D9B758534}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -435,62 +447,64 @@ Global
{78FC19B2-396C-4ED2-BFD9-6C5667C61666} = {DD020B34-460F-455F-8D17-CF4A949F100B}
{B615B353-476C-43B9-A776-B193B0DBD256} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{A11DC259-D1DB-4686-AD28-A427D0BABA83} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
- {2EC50C79-782D-4985-ABB1-AD07F35D1621} = {A11DC259-D1DB-4686-AD28-A427D0BABA83}
+ {2EC50C79-782D-4985-ABB1-AD07F35D1621} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
{15A16323-2CCA-472E-BE79-07259DAD5F6F} = {A11DC259-D1DB-4686-AD28-A427D0BABA83}
{5BACBA51-03FE-4CE1-B0F5-9E9C2A132FAB} = {A11DC259-D1DB-4686-AD28-A427D0BABA83}
{3160CC92-1D6E-42CB-AE89-9401C8CEC5CB} = {A11DC259-D1DB-4686-AD28-A427D0BABA83}
{02374BD0-BF0B-40F8-A04A-C4C4D61D4992} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
- {7957E852-1291-4FAA-9034-FB66CE817FF1} = {02374BD0-BF0B-40F8-A04A-C4C4D61D4992}
- {626D74DD-4F37-4F74-87A3-5A6888684F5E} = {02374BD0-BF0B-40F8-A04A-C4C4D61D4992}
+ {7957E852-1291-4FAA-9034-FB66CE817FF1} = {A11DC259-D1DB-4686-AD28-A427D0BABA83}
+ {626D74DD-4F37-4F74-87A3-5A6888684F5E} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
{CC0A5C98-ACDE-4139-BA2F-2995A9B8E18C} = {02374BD0-BF0B-40F8-A04A-C4C4D61D4992}
{A7F41094-8648-446B-AECD-DCC2CC871F73} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
- {F70AC78E-8925-4770-832A-2FC67A620EB2} = {A7F41094-8648-446B-AECD-DCC2CC871F73}
- {8B570E70-0E73-4042-A4B6-1CC3CC782A65} = {A7F41094-8648-446B-AECD-DCC2CC871F73}
- {4AA9E7B7-36BF-4AAE-BFA3-C9CE8740F4A0} = {DD020B34-460F-455F-8D17-CF4A949F100B}
- {345FC3FB-D1E9-4AE8-9052-17D20AB01FA2} = {DD020B34-460F-455F-8D17-CF4A949F100B}
- {2AED1542-A8ED-488D-B6D0-E16AB5D6EF6C} = {DD020B34-460F-455F-8D17-CF4A949F100B}
+ {F70AC78E-8925-4770-832A-2FC67A620EB2} = {02374BD0-BF0B-40F8-A04A-C4C4D61D4992}
+ {8B570E70-0E73-4042-A4B6-1CC3CC782A65} = {02374BD0-BF0B-40F8-A04A-C4C4D61D4992}
+ {4AA9E7B7-36BF-4AAE-BFA3-C9CE8740F4A0} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
+ {345FC3FB-D1E9-4AE8-9052-17D20AB01FA2} = {A7F41094-8648-446B-AECD-DCC2CC871F73}
+ {2AED1542-A8ED-488D-B6D0-E16AB5D6EF6C} = {A7F41094-8648-446B-AECD-DCC2CC871F73}
{E8212911-344B-4638-ADC3-B215BCDCAFD1} = {DD020B34-460F-455F-8D17-CF4A949F100B}
- {F80F837E-D2FC-4FFC-B68F-3CF0EC015F66} = {A7F41094-8648-446B-AECD-DCC2CC871F73}
+ {F80F837E-D2FC-4FFC-B68F-3CF0EC015F66} = {DD020B34-460F-455F-8D17-CF4A949F100B}
{5BE7F505-7D77-4C3A-ABFD-54088774DAA7} = {DD020B34-460F-455F-8D17-CF4A949F100B}
- {35031EDB-C0DE-453A-8335-D2EBEA2FC640} = {A7F41094-8648-446B-AECD-DCC2CC871F73}
+ {35031EDB-C0DE-453A-8335-D2EBEA2FC640} = {DD020B34-460F-455F-8D17-CF4A949F100B}
{07578B6C-9B96-4B3D-BA2E-7800EFCA7F99} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
- {5C61ABED-7623-4C28-A5C9-C5972A0F669C} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
+ {5C61ABED-7623-4C28-A5C9-C5972A0F669C} = {A7F41094-8648-446B-AECD-DCC2CC871F73}
{0EF6EA64-D7C3-420D-9890-EAE8D54A57E6} = {A7F41094-8648-446B-AECD-DCC2CC871F73}
- {4A175C27-EAFE-47E7-90F6-873B37863656} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6}
- {DDC41278-FB60-403A-B969-2AEBD7C2D83C} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6}
- {8CA09061-2BEF-4506-A763-07062D2BD6AC} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
+ {4A175C27-EAFE-47E7-90F6-873B37863656} = {DD020B34-460F-455F-8D17-CF4A949F100B}
+ {DDC41278-FB60-403A-B969-2AEBD7C2D83C} = {A7F41094-8648-446B-AECD-DCC2CC871F73}
+ {8CA09061-2BEF-4506-A763-07062D2BD6AC} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
{7592AFA4-426B-42F3-AE82-957C86814482} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
- {61C24126-F39D-4BEA-96DC-FC87BA730554} = {7592AFA4-426B-42F3-AE82-957C86814482}
- {CB903D21-4869-42EF-BDD6-5B1CFF674337} = {7592AFA4-426B-42F3-AE82-957C86814482}
- {980B5FD8-0107-41F7-8FAD-E4E8BAE8A625} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
- {7C06FE2D-6C62-48F5-A505-F0D715C554DE} = {7592AFA4-426B-42F3-AE82-957C86814482}
- {AF89083D-4715-42E6-93E9-38497D12A8A6} = {DD020B34-460F-455F-8D17-CF4A949F100B}
- {B5CDB0DC-B26D-48F1-B934-FE5C1C991940} = {DD020B34-460F-455F-8D17-CF4A949F100B}
- {C74FBA78-13E8-407F-A173-4555AEE41FF3} = {A7F41094-8648-446B-AECD-DCC2CC871F73}
- {DFBABB04-50E9-42F6-B470-310E1B545638} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
+ {61C24126-F39D-4BEA-96DC-FC87BA730554} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
+ {CB903D21-4869-42EF-BDD6-5B1CFF674337} = {A7F41094-8648-446B-AECD-DCC2CC871F73}
+ {980B5FD8-0107-41F7-8FAD-E4E8BAE8A625} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6}
+ {7C06FE2D-6C62-48F5-A505-F0D715C554DE} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6}
+ {AF89083D-4715-42E6-93E9-38497D12A8A6} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
+ {B5CDB0DC-B26D-48F1-B934-FE5C1C991940} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
+ {C74FBA78-13E8-407F-A173-4555AEE41FF3} = {7592AFA4-426B-42F3-AE82-957C86814482}
+ {DFBABB04-50E9-42F6-B470-310E1B545638} = {7592AFA4-426B-42F3-AE82-957C86814482}
{B445B19C-A925-4873-8CB7-8317898B6970} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
- {CDB47863-BEBD-4841-A807-46D868962521} = {DD020B34-460F-455F-8D17-CF4A949F100B}
- {273F2527-1658-4CCF-8DC6-600E921188C5} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
+ {CDB47863-BEBD-4841-A807-46D868962521} = {7592AFA4-426B-42F3-AE82-957C86814482}
+ {273F2527-1658-4CCF-8DC6-600E921188C5} = {DD020B34-460F-455F-8D17-CF4A949F100B}
{2F3700EF-1CDA-4C15-AC88-360230000ECD} = {DD020B34-460F-455F-8D17-CF4A949F100B}
{3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
- {11011FF8-77EA-4B25-96C0-29D4D486EF1C} = {3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5}
- {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
- {7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
- {945DD3B7-94E5-435E-B3CB-796C20A652C7} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
- {FD3E9371-3134-4235-8E80-32226DFB4B1F} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
- {D83B27F3-4401-42F5-843E-147566B4999A} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
- {00359961-0C50-4BB1-A794-8B06DE991639} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
- {4E04EB35-7FD2-4FDB-B09A-F75CE24053B9} = {DD020B34-460F-455F-8D17-CF4A949F100B}
- {0EAE36A1-B578-4F13-A113-7A477ECA1BDA} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
- {290D1278-F613-4DF3-9DF5-F37E38CDC363} = {0EF6EA64-D7C3-420D-9890-EAE8D54A57E6}
- {C8BB6A85-A7EA-40C0-893D-F36F317829B3} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
- {BF9828E9-5597-4D42-AA6E-6E6C12214204} = {DD020B34-460F-455F-8D17-CF4A949F100B}
+ {11011FF8-77EA-4B25-96C0-29D4D486EF1C} = {A7F41094-8648-446B-AECD-DCC2CC871F73}
+ {43CB06A9-7E88-4C5F-BFB8-947E072CBC9F} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
+ {7F73A3D8-FFC2-4E31-AA3D-A4840316A8C6} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
+ {945DD3B7-94E5-435E-B3CB-796C20A652C7} = {DD020B34-460F-455F-8D17-CF4A949F100B}
+ {FD3E9371-3134-4235-8E80-32226DFB4B1F} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
+ {D83B27F3-4401-42F5-843E-147566B4999A} = {DD020B34-460F-455F-8D17-CF4A949F100B}
+ {00359961-0C50-4BB1-A794-8B06DE991639} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
+ {4E04EB35-7FD2-4FDB-B09A-F75CE24053B9} = {3046DBF4-C2FF-4F3A-9176-E1C01E0A90E5}
+ {0EAE36A1-B578-4F13-A113-7A477ECA1BDA} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
+ {290D1278-F613-4DF3-9DF5-F37E38CDC363} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
+ {C8BB6A85-A7EA-40C0-893D-F36F317829B3} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
+ {BF9828E9-5597-4D42-AA6E-6E6C12214204} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
{D9697361-232F-465D-A136-4561E0E88488} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
- {9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {D9697361-232F-465D-A136-4561E0E88488}
- {E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {DD020B34-460F-455F-8D17-CF4A949F100B}
+ {9CAF360E-5AD3-4C4F-89A0-327EEB70D673} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
+ {E90114C6-86FC-43B8-AE5C-D9273CF21FE4} = {BF3ED6BF-ADF3-4D25-8E89-02FB8D945CA9}
{55A7D436-CC8C-47E6-B43A-DFE32E0FE38C} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{CE0D5FEB-F6DB-4EB8-B8A9-6A4A32944539} = {DD020B34-460F-455F-8D17-CF4A949F100B}
+ {984BBCC6-C827-430A-8796-C4EB55E7D979} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
+ {A716C2AB-1B38-4284-AC94-2C3D9B758534} = {DD020B34-460F-455F-8D17-CF4A949F100B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}
diff --git a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj
index 12b512fbb..ec158a43d 100644
--- a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj
+++ b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj
@@ -12,6 +12,7 @@
+
diff --git a/src/Dapr.Pubsub.Analyzers/AnalyzerReleases.Shipped.md b/src/Dapr.Pubsub.Analyzers/AnalyzerReleases.Shipped.md
new file mode 100644
index 000000000..0db9df24c
--- /dev/null
+++ b/src/Dapr.Pubsub.Analyzers/AnalyzerReleases.Shipped.md
@@ -0,0 +1,7 @@
+## Release 1.16
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|--------------------
+DAPR2001| Usage | Warning | Call MapSubscribeHandler
\ No newline at end of file
diff --git a/src/Dapr.Pubsub.Analyzers/AnalyzerReleases.Unshipped.md b/src/Dapr.Pubsub.Analyzers/AnalyzerReleases.Unshipped.md
new file mode 100644
index 000000000..f2b7fad65
--- /dev/null
+++ b/src/Dapr.Pubsub.Analyzers/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,2 @@
+; Unshipped analyzer release
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
diff --git a/src/Dapr.Pubsub.Analyzers/Dapr.Pubsub.Analyzers.csproj b/src/Dapr.Pubsub.Analyzers/Dapr.Pubsub.Analyzers.csproj
new file mode 100644
index 000000000..4775012db
--- /dev/null
+++ b/src/Dapr.Pubsub.Analyzers/Dapr.Pubsub.Analyzers.csproj
@@ -0,0 +1,38 @@
+
+
+
+ netstandard2.0
+
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+ true
+
+
+ false
+
+
+ false
+
+
+ This package contains Roslyn analyzers for actors.
+ $(PackageTags)
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Dapr.Pubsub.Analyzers/MapSubscribeHandlerCodeFixProvider.cs b/src/Dapr.Pubsub.Analyzers/MapSubscribeHandlerCodeFixProvider.cs
new file mode 100644
index 000000000..38dae50ba
--- /dev/null
+++ b/src/Dapr.Pubsub.Analyzers/MapSubscribeHandlerCodeFixProvider.cs
@@ -0,0 +1,114 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Dapr.Pubsub.Analyzers;
+
+///
+/// Provides a code fix for the DAPR2001 diagnostic.
+///
+public class MapSubscribeHandlerCodeFixProvider : CodeFixProvider
+{
+ ///
+ /// Gets the diagnostic IDs that this provider can fix.
+ ///
+ public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create("DAPR2001");
+
+ ///
+ /// Gets the FixAllProvider for this code fix provider.
+ ///
+ /// The FixAllProvider.
+ public override FixAllProvider? GetFixAllProvider()
+ {
+ return WellKnownFixAllProviders.BatchFixer;
+ }
+
+ ///
+ /// Registers code fixes for the specified diagnostics.
+ ///
+ /// A containing the context in which the code fix is being applied.
+ public override Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var title = "Call MapSubscribeHandler";
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title,
+ createChangedDocument: c => AddMapSubscribeHandlerAsync(context.Document, context.Diagnostics.First(), c),
+ equivalenceKey: title),
+ context.Diagnostics);
+ return Task.CompletedTask;
+ }
+
+ private async Task AddMapSubscribeHandlerAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
+ {
+ var root = await document.GetSyntaxRootAsync(cancellationToken);
+ var invocationExpressions = root!.DescendantNodes().OfType();
+
+ var createBuilderInvocation = invocationExpressions
+ .FirstOrDefault(invocation =>
+ {
+ return invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
+ memberAccess.Name.Identifier.Text == "CreateBuilder" &&
+ memberAccess.Expression is IdentifierNameSyntax identifier &&
+ identifier.Identifier.Text == "WebApplication";
+ });
+
+ var variableDeclarator = createBuilderInvocation
+ .AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault();
+
+ var variableName = variableDeclarator.Identifier.Text;
+
+ var buildInvocation = invocationExpressions
+ .FirstOrDefault(invocation =>
+ {
+ return invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
+ memberAccess.Name.Identifier.Text == "Build" &&
+ memberAccess.Expression is IdentifierNameSyntax identifier &&
+ identifier.Identifier.Text == variableName;
+ });
+
+ var buildVariableDeclarator = buildInvocation
+ .AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault();
+
+ var buildVariableName = buildVariableDeclarator.Identifier.Text;
+
+ var mapSubscribeHandlerInvocation = SyntaxFactory.ExpressionStatement(
+ SyntaxFactory.InvocationExpression(
+ SyntaxFactory.MemberAccessExpression(
+ SyntaxKind.SimpleMemberAccessExpression,
+ SyntaxFactory.IdentifierName(buildVariableName),
+ SyntaxFactory.IdentifierName("MapSubscribeHandler"))));
+
+ if (buildInvocation?.Ancestors().OfType().FirstOrDefault() is SyntaxNode parentBlock)
+ {
+ var localDeclaration = buildInvocation
+ .AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault();
+
+ var newParentBlock = parentBlock.InsertNodesAfter(localDeclaration, new[] { mapSubscribeHandlerInvocation });
+ root = root.ReplaceNode(parentBlock, newParentBlock);
+ }
+ else
+ {
+ var buildInvocationGlobalStatement = buildInvocation?
+ .AncestorsAndSelf()
+ .OfType()
+ .FirstOrDefault();
+
+ var compilationUnitSyntax = createBuilderInvocation.Ancestors().OfType().FirstOrDefault();
+ var newCompilationUnitSyntax = compilationUnitSyntax.InsertNodesAfter(buildInvocationGlobalStatement!,
+ new[] { SyntaxFactory.GlobalStatement(mapSubscribeHandlerInvocation) });
+ root = root.ReplaceNode(compilationUnitSyntax, newCompilationUnitSyntax);
+ }
+
+ return document.WithSyntaxRoot(root);
+ }
+}
diff --git a/src/Dapr.Pubsub.Analyzers/SubscriptionAnalyzer.cs b/src/Dapr.Pubsub.Analyzers/SubscriptionAnalyzer.cs
new file mode 100644
index 000000000..1ddc17a44
--- /dev/null
+++ b/src/Dapr.Pubsub.Analyzers/SubscriptionAnalyzer.cs
@@ -0,0 +1,139 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace Dapr.Pubsub.Analyzers;
+
+///
+/// Analyzes the subscription methods to ensure proper usage of MapSubscribeHandler.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class SubscriptionAnalyzer : DiagnosticAnalyzer
+{
+ private static readonly DiagnosticDescriptor DiagnosticDescriptorMapSubscribeHandler = new(
+ "DAPR2001",
+ "Call MapSubscribeHandler",
+ "Call app.MapSubscribeHandler to map endpoints for Dapr subscriptions",
+ "Usage",
+ DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ ///
+ /// Gets the supported diagnostics for this analyzer.
+ ///
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(DiagnosticDescriptorMapSubscribeHandler);
+
+ ///
+ /// Initializes the analyzer.
+ ///
+ /// The analysis context.
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+ context.RegisterSyntaxNodeAction(AnalyzeMapSubscribeHandler, SyntaxKind.CompilationUnit);
+ }
+
+ private void AnalyzeMapSubscribeHandler(SyntaxNodeAnalysisContext context)
+ {
+ var withTopicInvocations = FindInvocations(context, "WithTopic");
+ var methodsWithTopicAttribute = FindMethodsWithTopicAttribute(context);
+ var invocationsWithTopicAttribute = FindInvocationsWithTopicAttribute(context);
+
+ bool invokedByWebApplication = false;
+ var mapSubscribeHandlerInvocation = FindInvocations(context, "MapSubscribeHandler")?.FirstOrDefault();
+
+ if (mapSubscribeHandlerInvocation?.Expression is MemberAccessExpressionSyntax memberAccess)
+ {
+ var symbolInfo = context.SemanticModel.GetSymbolInfo(memberAccess.Expression);
+ if (symbolInfo.Symbol is ILocalSymbol localSymbol)
+ {
+ var type = localSymbol.Type;
+ if (type.ToDisplayString() == "Microsoft.AspNetCore.Builder.WebApplication")
+ {
+ invokedByWebApplication = true;
+ }
+ }
+ }
+
+ foreach (var withTopicInvocation in withTopicInvocations)
+ {
+ if (mapSubscribeHandlerInvocation == null || !invokedByWebApplication)
+ {
+ var diagnostic = Diagnostic.Create(DiagnosticDescriptorMapSubscribeHandler, withTopicInvocation.GetLocation());
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ foreach (var methodWithTopicAttribute in methodsWithTopicAttribute)
+ {
+ if (mapSubscribeHandlerInvocation == null || !invokedByWebApplication)
+ {
+ var diagnostic = Diagnostic.Create(DiagnosticDescriptorMapSubscribeHandler, methodWithTopicAttribute.GetLocation());
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ foreach (var invocationWithTopicAttribute in invocationsWithTopicAttribute)
+ {
+ if (mapSubscribeHandlerInvocation == null || !invokedByWebApplication)
+ {
+ var diagnostic = Diagnostic.Create(DiagnosticDescriptorMapSubscribeHandler, invocationWithTopicAttribute.GetLocation());
+ context.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+
+ private List FindInvocations(SyntaxNodeAnalysisContext context, string methodName)
+ {
+ var invocations = new List();
+
+ foreach (var syntaxTree in context.SemanticModel.Compilation.SyntaxTrees)
+ {
+ var root = syntaxTree.GetRoot();
+ invocations.AddRange(root.DescendantNodes().OfType()
+ .Where(invocation => invocation.Expression is MemberAccessExpressionSyntax memberAccess &&
+ memberAccess.Name.Identifier.Text == methodName));
+ }
+
+ return invocations;
+ }
+
+ private List FindMethodsWithTopicAttribute(SyntaxNodeAnalysisContext context)
+ {
+ var methodsWithTopicAttribute = new List();
+
+ foreach (var syntaxTree in context.SemanticModel.Compilation.SyntaxTrees)
+ {
+ var root = syntaxTree.GetRoot();
+ methodsWithTopicAttribute.AddRange(root.DescendantNodes()
+ .OfType()
+ .Where(method => method.AttributeLists
+ .SelectMany(attributeList => attributeList.Attributes)
+ .Any(attribute => attribute.Name.ToString() == "Topic" || attribute.Name.ToString().EndsWith(".Topic"))));
+ }
+
+ return methodsWithTopicAttribute;
+ }
+
+ private List FindInvocationsWithTopicAttribute(SyntaxNodeAnalysisContext context)
+ {
+ var invocationsWithTopicAttributeParameter = new List();
+
+ foreach (var syntaxTree in context.SemanticModel.Compilation.SyntaxTrees)
+ {
+ var root = syntaxTree.GetRoot();
+ invocationsWithTopicAttributeParameter.AddRange(root.DescendantNodes()
+ .OfType()
+ .Where(invocation => invocation.ArgumentList.Arguments
+ .Any(argument => argument.Expression is ParenthesizedLambdaExpressionSyntax parenthesizedLambdaExpression &&
+ parenthesizedLambdaExpression.AttributeLists
+ .SelectMany(attributeList => attributeList.Attributes)
+ .Any(attribute => attribute.Name.ToString() == "Topic" || attribute.Name.ToString().EndsWith(".Topic")))));
+ }
+
+ return invocationsWithTopicAttributeParameter;
+ }
+}
diff --git a/test/Dapr.Pubsub.Analyzers.Test/Dapr.Pubsub.Analyzers.Test.csproj b/test/Dapr.Pubsub.Analyzers.Test/Dapr.Pubsub.Analyzers.Test.csproj
new file mode 100644
index 000000000..ee8315f20
--- /dev/null
+++ b/test/Dapr.Pubsub.Analyzers.Test/Dapr.Pubsub.Analyzers.Test.csproj
@@ -0,0 +1,35 @@
+
+
+
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Dapr.Pubsub.Analyzers.Test/MapSubscribeHandlerCodeFixProviderTests.cs b/test/Dapr.Pubsub.Analyzers.Test/MapSubscribeHandlerCodeFixProviderTests.cs
new file mode 100644
index 000000000..5a50012d5
--- /dev/null
+++ b/test/Dapr.Pubsub.Analyzers.Test/MapSubscribeHandlerCodeFixProviderTests.cs
@@ -0,0 +1,72 @@
+namespace Dapr.Pubsub.Analyzers.Test;
+
+public class MapSubscribeHandlerCodeFixProviderTests
+{
+ [Fact]
+ public async Task MapSubscribeHandler()
+ {
+ var code = @"
+ using Microsoft.AspNetCore.Builder;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+ var app = builder.Build();
+
+ app.MapPost(""/subscribe"", () => {})
+ .WithTopic(""pubSubName"", ""topicName"");
+ }
+ }
+ ";
+
+ var expectedChangedCode = @"
+ using Microsoft.AspNetCore.Builder;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+ var app = builder.Build();
+
+ app.MapSubscribeHandler();
+
+ app.MapPost(""/subscribe"", () => {})
+ .WithTopic(""pubSubName"", ""topicName"");
+ }
+ }
+ ";
+
+ await VerifyCodeFix.RunTest(code, expectedChangedCode);
+ }
+
+ [Fact]
+ public async Task MapSubscribeHandler_TopLevelStatements()
+ {
+ var code = @"
+ using Microsoft.AspNetCore.Builder;
+
+ var builder = WebApplication.CreateBuilder();
+ var app = builder.Build();
+
+ app.MapPost(""/subscribe"", () => {})
+ .WithTopic(""pubSubName"", ""topicName"");
+ ";
+
+ var expectedChangedCode = @"
+ using Microsoft.AspNetCore.Builder;
+
+ var builder = WebApplication.CreateBuilder();
+ var app = builder.Build();
+
+ app.MapSubscribeHandler();
+
+ app.MapPost(""/subscribe"", () => {})
+ .WithTopic(""pubSubName"", ""topicName"");
+ ";
+
+ await VerifyCodeFix.RunTest(code, expectedChangedCode);
+ }
+}
diff --git a/test/Dapr.Pubsub.Analyzers.Test/SubscriptionAnalyzerTests.cs b/test/Dapr.Pubsub.Analyzers.Test/SubscriptionAnalyzerTests.cs
new file mode 100644
index 000000000..3712b0b2a
--- /dev/null
+++ b/test/Dapr.Pubsub.Analyzers.Test/SubscriptionAnalyzerTests.cs
@@ -0,0 +1,60 @@
+using Microsoft.CodeAnalysis;
+
+namespace Dapr.Pubsub.Analyzers.Test;
+
+public class SubscriptionAnalyzerTests
+{
+ public class MapSubscribeHandler
+ {
+ [Fact]
+ public async Task ReportDiagnostic_WithTopic()
+ {
+ var testCode = @"
+ using Microsoft.AspNetCore.Builder;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+ var app = builder.Build();
+
+ app.MapPost(""/subscribe"", () => {})
+ .WithTopic(""pubSubName"", ""topicName"");
+ }
+ }
+ ";
+
+ var expected = VerifyAnalyzer.Diagnostic("DAPR2001", DiagnosticSeverity.Warning)
+ .WithSpan(11, 25, 12, 66).WithMessage("Call app.MapSubscribeHandler to map endpoints for Dapr subscriptions");
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ }
+
+ [Fact]
+ public async Task ReportDiagnostic_TopicAttribute()
+ {
+ var testCode = @"
+ using Dapr;
+ using Microsoft.AspNetCore.Builder;
+
+ public static class Program
+ {
+ public static void Main()
+ {
+ var builder = WebApplication.CreateBuilder();
+ var app = builder.Build();
+
+ app.MapPost(""/subscribe"", [Topic(""pubsubName"", ""topicName"")] () => {});
+ }
+ }
+ ";
+
+ var expected = VerifyAnalyzer.Diagnostic("DAPR2001", DiagnosticSeverity.Warning)
+ .WithSpan(12, 25, 12, 95).WithMessage("Call app.MapSubscribeHandler to map endpoints for Dapr subscriptions");
+
+ await VerifyAnalyzer.VerifyAnalyzerAsync(testCode, expected);
+ }
+ }
+
+}
diff --git a/test/Dapr.Pubsub.Analyzers.Test/Utilities.cs b/test/Dapr.Pubsub.Analyzers.Test/Utilities.cs
new file mode 100644
index 000000000..25cbefe57
--- /dev/null
+++ b/test/Dapr.Pubsub.Analyzers.Test/Utilities.cs
@@ -0,0 +1,75 @@
+using System.Collections.Immutable;
+using System.Reflection;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis;
+using Microsoft.Extensions.Hosting;
+using Microsoft.AspNetCore.Builder;
+
+namespace Dapr.Pubsub.Analyzers.Test;
+
+internal static class Utilities
+{
+ public static async Task<(ImmutableArray diagnostics, Document document, Workspace workspace)> GetDiagnosticsAdvanced(string code)
+ {
+ var workspace = new AdhocWorkspace();
+
+#if NET6_0
+ var referenceAssemblies = ReferenceAssemblies.Net.Net60;
+#elif NET7_0
+ var referenceAssemblies = ReferenceAssemblies.Net.Net70;
+#elif NET8_0
+ var referenceAssemblies = ReferenceAssemblies.Net.Net80;
+#elif NET9_0
+ var referenceAssemblies = ReferenceAssemblies.Net.Net90;
+#endif
+
+ // Create a new project with necessary references
+ var project = workspace.AddProject("TestProject", LanguageNames.CSharp)
+ .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.ConsoleApplication)
+ .WithSpecificDiagnosticOptions(new Dictionary
+ {
+ { "CS1701", ReportDiagnostic.Suppress }
+ }))
+ //.AddMetadataReferences(await referenceAssemblies.ResolveAsync(LanguageNames.CSharp, default))
+ .AddMetadataReference(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
+ .AddMetadataReferences(GetAllReferencesNeededForType(typeof(WebApplication)))
+ .AddMetadataReferences(GetAllReferencesNeededForType(typeof(IHost)))
+ .AddMetadataReferences(GetAllReferencesNeededForType(typeof(DaprEndpointConventionBuilderExtensions)));
+
+ // Add the document to the project
+ var document = project.AddDocument("TestDocument.cs", code);
+
+ // Get the syntax tree and create a compilation
+ var syntaxTree = await document.GetSyntaxTreeAsync() ?? throw new InvalidOperationException("Syntax tree is null");
+ var compilation = CSharpCompilation.Create("TestCompilation")
+ .AddSyntaxTrees(syntaxTree)
+ .AddReferences(project.MetadataReferences)
+ .WithOptions(project.CompilationOptions!);
+
+ var compilationWithAnalyzer = compilation.WithAnalyzers(
+ ImmutableArray.Create(
+ new SubscriptionAnalyzer()));
+
+ // Get diagnostics from the compilation
+ var diagnostics = await compilationWithAnalyzer.GetAllDiagnosticsAsync();
+ return (diagnostics, document, workspace);
+ }
+
+ public static MetadataReference[] GetAllReferencesNeededForType(Type type)
+ {
+ var files = GetAllAssemblyFilesNeededForType(type);
+
+ return files.Select(x => MetadataReference.CreateFromFile(x)).Cast().ToArray();
+ }
+
+ private static ImmutableArray GetAllAssemblyFilesNeededForType(Type type)
+ {
+ return type.Assembly.GetReferencedAssemblies()
+ .Select(x => Assembly.Load(x.FullName))
+ .Append(type.Assembly)
+ .Select(x => x.Location)
+ .ToImmutableArray();
+ }
+}
diff --git a/test/Dapr.Pubsub.Analyzers.Test/VerifyAnalyzer.cs b/test/Dapr.Pubsub.Analyzers.Test/VerifyAnalyzer.cs
new file mode 100644
index 000000000..8ca244348
--- /dev/null
+++ b/test/Dapr.Pubsub.Analyzers.Test/VerifyAnalyzer.cs
@@ -0,0 +1,69 @@
+using Microsoft.CodeAnalysis.CSharp.Testing;
+using Microsoft.CodeAnalysis.Testing;
+using Microsoft.CodeAnalysis;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.Hosting;
+
+namespace Dapr.Pubsub.Analyzers.Test;
+
+internal static class VerifyAnalyzer
+{
+ public static DiagnosticResult Diagnostic(string diagnosticId, DiagnosticSeverity diagnosticSeverity)
+ {
+ return new DiagnosticResult(diagnosticId, diagnosticSeverity);
+ }
+
+ public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
+ {
+ await VerifyAnalyzerAsync(source, null, expected);
+ }
+
+ public static async Task VerifyAnalyzerAsync(string source, string? program, params DiagnosticResult[] expected)
+ {
+ var test = new Test { TestCode = source };
+
+#if NET6_0
+ test.ReferenceAssemblies = ReferenceAssemblies.Net.Net60;
+#elif NET7_0
+ test.ReferenceAssemblies = ReferenceAssemblies.Net.Net70;
+#elif NET8_0
+ test.ReferenceAssemblies = ReferenceAssemblies.Net.Net80;
+#elif NET9_0
+ test.ReferenceAssemblies = ReferenceAssemblies.Net.Net90;
+#endif
+
+ if (program != null)
+ {
+ test.TestState.Sources.Add(("Program.cs", program));
+ }
+
+ var metadataReferences = Utilities.GetAllReferencesNeededForType(typeof(SubscriptionAnalyzer)).ToList();
+ //metadataReferences.AddRange(await test.ReferenceAssemblies.ResolveAsync(LanguageNames.CSharp, default));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location));
+ metadataReferences.AddRange(Utilities.GetAllReferencesNeededForType(typeof(WebApplication)));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(IHost).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(EndpointRouteBuilderExtensions).Assembly.Location));
+ metadataReferences.Add(MetadataReference.CreateFromFile(typeof(DaprEndpointConventionBuilderExtensions).Assembly.Location));
+
+ foreach (var reference in metadataReferences)
+ {
+ test.TestState.AdditionalReferences.Add(reference);
+ }
+
+ test.ExpectedDiagnostics.AddRange(expected);
+ await test.RunAsync(CancellationToken.None);
+ }
+
+ private class Test : CSharpAnalyzerTest
+ {
+ public Test()
+ {
+ SolutionTransforms.Add((solution, projectId) =>
+ {
+ var compilationOptions = solution.GetProject(projectId)!.CompilationOptions!;
+ solution = solution.WithProjectCompilationOptions(projectId, compilationOptions);
+ return solution;
+ });
+ }
+ }
+}
diff --git a/test/Dapr.Pubsub.Analyzers.Test/VerifyCodeFix.cs b/test/Dapr.Pubsub.Analyzers.Test/VerifyCodeFix.cs
new file mode 100644
index 000000000..593abacda
--- /dev/null
+++ b/test/Dapr.Pubsub.Analyzers.Test/VerifyCodeFix.cs
@@ -0,0 +1,56 @@
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+
+namespace Dapr.Pubsub.Analyzers.Test;
+
+internal class VerifyCodeFix
+{
+ public static async Task RunTest(string code, string expectedChangedCode) where T : CodeFixProvider, new()
+ {
+ var (diagnostics, document, workspace) = await Utilities.GetDiagnosticsAdvanced(code);
+
+ Assert.Single(diagnostics);
+
+ var diagnostic = diagnostics[0];
+
+ var codeFixProvider = new T();
+
+ CodeAction? registeredCodeAction = null;
+
+ var context = new CodeFixContext(document, diagnostic, (codeAction, _) =>
+ {
+ if (registeredCodeAction != null)
+ throw new Exception("Code action was registered more than once");
+
+ registeredCodeAction = codeAction;
+
+ }, CancellationToken.None);
+
+ await codeFixProvider.RegisterCodeFixesAsync(context);
+
+ if (registeredCodeAction == null)
+ throw new Exception("Code action was not registered");
+
+ var operations = await registeredCodeAction.GetOperationsAsync(CancellationToken.None);
+
+ foreach (var operation in operations)
+ {
+ operation.Apply(workspace, CancellationToken.None);
+ }
+
+ var updatedDocument = workspace.CurrentSolution.GetDocument(document.Id) ?? throw new Exception("Updated document is null");
+ var newCode = (await updatedDocument.GetTextAsync()).ToString();
+
+ // Normalize whitespace
+ string NormalizeWhitespace(string input)
+ {
+ var separator = new[] { ' ', '\r', '\n' };
+ return string.Join(" ", input.Split(separator, StringSplitOptions.RemoveEmptyEntries));
+ }
+
+ var normalizedExpectedCode = NormalizeWhitespace(expectedChangedCode);
+ var normalizedNewCode = NormalizeWhitespace(newCode);
+
+ Assert.Equal(normalizedExpectedCode, normalizedNewCode);
+ }
+}