diff --git a/src/Custom.Build.props b/src/Custom.Build.props
index 96047fdf..dd483374 100644
--- a/src/Custom.Build.props
+++ b/src/Custom.Build.props
@@ -2,6 +2,7 @@
0.9.0
+ netstandard2.0
diff --git a/src/NServiceBus.AzureFunctions.Analyzer.Tests/AnalyzerTestFixture.cs b/src/NServiceBus.AzureFunctions.Analyzer.Tests/AnalyzerTestFixture.cs
new file mode 100644
index 00000000..de379254
--- /dev/null
+++ b/src/NServiceBus.AzureFunctions.Analyzer.Tests/AnalyzerTestFixture.cs
@@ -0,0 +1,214 @@
+namespace NServiceBus.AzureFunctions.Analyzer.Tests
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Immutable;
+ using System.Linq;
+ using System.Reflection;
+ using System.Text;
+ using System.Text.RegularExpressions;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.CodeAnalysis;
+ using Microsoft.CodeAnalysis.CSharp;
+ using Microsoft.CodeAnalysis.Diagnostics;
+ using Microsoft.CodeAnalysis.Text;
+
+ public class AnalyzerTestFixture where TAnalyzer : DiagnosticAnalyzer, new()
+ {
+ protected virtual LanguageVersion AnalyzerLanguageVersion => LanguageVersion.CSharp7;
+
+ protected Task Assert(string markupCode, CancellationToken cancellationToken = default) =>
+ Assert(Array.Empty(), markupCode, Array.Empty(), cancellationToken);
+
+ protected Task Assert(string expectedDiagnosticId, string markupCode, CancellationToken cancellationToken = default) =>
+ Assert(new[] { expectedDiagnosticId }, markupCode, Array.Empty(), cancellationToken);
+
+ protected async Task Assert(string[] expectedDiagnosticIds, string markupCode, string[] ignoreDiagnosticIds, CancellationToken cancellationToken = default)
+ {
+ var (code, markupSpans) = Parse(markupCode);
+
+ var project = CreateProject(code);
+ await WriteCode(project);
+
+ var compilerDiagnostics = (await Task.WhenAll(project.Documents
+ .Select(doc => doc.GetCompilerDiagnostics(cancellationToken))))
+ .SelectMany(diagnostics => diagnostics);
+
+ WriteCompilerDiagnostics(compilerDiagnostics);
+
+ var compilation = await project.GetCompilationAsync(cancellationToken);
+ compilation.Compile();
+
+ var analyzerDiagnostics = (await compilation.GetAnalyzerDiagnostics(new TAnalyzer(), cancellationToken))
+ .Where(d => !ignoreDiagnosticIds.Contains(d.Id))
+ .ToList();
+ WriteAnalyzerDiagnostics(analyzerDiagnostics);
+
+ var expectedSpansAndIds = expectedDiagnosticIds
+ .SelectMany(id => markupSpans.Select(span => (span.file, span.span, id)))
+ .OrderBy(item => item.span)
+ .ThenBy(item => item.id)
+ .ToList();
+
+ var actualSpansAndIds = analyzerDiagnostics
+ .Select(diagnostic => (diagnostic.Location.SourceTree.FilePath, diagnostic.Location.SourceSpan, diagnostic.Id))
+ .ToList();
+
+ NUnit.Framework.CollectionAssert.AreEqual(expectedSpansAndIds, actualSpansAndIds);
+ }
+
+ protected static async Task WriteCode(Project project)
+ {
+ if (!VerboseLogging)
+ {
+ return;
+ }
+
+ foreach (var document in project.Documents)
+ {
+ Console.WriteLine(document.Name);
+ var code = await document.GetCode();
+ foreach (var (line, index) in code.Replace("\r\n", "\n").Split('\n')
+ .Select((line, index) => (line, index)))
+ {
+ Console.WriteLine($" {index + 1,3}: {line}");
+ }
+ }
+
+ }
+
+ static readonly ImmutableDictionary DiagnosticOptions = new Dictionary
+ {
+ { "CS1701", ReportDiagnostic.Hidden }
+ }
+ .ToImmutableDictionary();
+
+ protected Project CreateProject(string[] code)
+ {
+ var workspace = new AdhocWorkspace();
+ var project = workspace.AddProject("TestProject", LanguageNames.CSharp)
+ .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
+ .WithSpecificDiagnosticOptions(DiagnosticOptions))
+ .WithParseOptions(new CSharpParseOptions(AnalyzerLanguageVersion))
+ .AddMetadataReferences(ProjectReferences);
+
+ for (int i = 0; i < code.Length; i++)
+ {
+ project = project.AddDocument($"TestDocument{i}", code[i]).Project;
+ }
+
+ return project;
+ }
+
+ static AnalyzerTestFixture()
+ {
+ ProjectReferences = ImmutableList.Create(
+ MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(Enumerable).GetTypeInfo().Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(System.Linq.Expressions.Expression).GetTypeInfo().Assembly
+ .Location),
+#if NET
+ MetadataReference.CreateFromFile(Assembly.Load("System.Runtime").Location),
+#endif
+ MetadataReference.CreateFromFile(typeof(IFunctionEndpoint).GetTypeInfo().Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(EndpointConfiguration).GetTypeInfo().Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(AzureServiceBusTransport).GetTypeInfo().Assembly.Location));
+ }
+
+ static readonly ImmutableList ProjectReferences;
+
+ static readonly Regex DocumentSplittingRegex = new Regex("^-{5,}.*", RegexOptions.Compiled | RegexOptions.Multiline);
+
+ protected static void WriteCompilerDiagnostics(IEnumerable diagnostics)
+ {
+ if (!VerboseLogging)
+ {
+ return;
+ }
+
+ Console.WriteLine("Compiler diagnostics:");
+
+ foreach (var diagnostic in diagnostics)
+ {
+ Console.WriteLine($" {diagnostic}");
+ }
+ }
+
+ protected static void WriteAnalyzerDiagnostics(IEnumerable diagnostics)
+ {
+ if (!VerboseLogging)
+ {
+ return;
+ }
+
+ Console.WriteLine("Analyzer diagnostics:");
+
+ foreach (var diagnostic in diagnostics)
+ {
+ Console.WriteLine($" {diagnostic}");
+ }
+ }
+
+ protected static string[] SplitMarkupCodeIntoFiles(string markupCode)
+ {
+ return DocumentSplittingRegex.Split(markupCode)
+ .Where(docCode => !string.IsNullOrWhiteSpace(docCode))
+ .ToArray();
+ }
+
+ static (string[] code, List<(string file, TextSpan span)>) Parse(string markupCode)
+ {
+ if (markupCode == null)
+ {
+ return (Array.Empty(), new List<(string, TextSpan)>());
+ }
+
+ var documents = SplitMarkupCodeIntoFiles(markupCode);
+
+ var markupSpans = new List<(string, TextSpan)>();
+
+ for (var i = 0; i < documents.Length; i++)
+ {
+ var code = new StringBuilder();
+ var name = $"TestDocument{i}";
+
+ var remainingCode = documents[i];
+ var remainingCodeStart = 0;
+
+ while (remainingCode.Length > 0)
+ {
+ var beforeAndAfterOpening = remainingCode.Split(new[] { "[|" }, 2, StringSplitOptions.None);
+
+ if (beforeAndAfterOpening.Length == 1)
+ {
+ _ = code.Append(beforeAndAfterOpening[0]);
+ break;
+ }
+
+ var midAndAfterClosing = beforeAndAfterOpening[1].Split(new[] { "|]" }, 2, StringSplitOptions.None);
+
+ if (midAndAfterClosing.Length == 1)
+ {
+ throw new Exception("The markup code does not contain a closing '|]'");
+ }
+
+ var markupSpan = new TextSpan(remainingCodeStart + beforeAndAfterOpening[0].Length, midAndAfterClosing[0].Length);
+
+ _ = code.Append(beforeAndAfterOpening[0]).Append(midAndAfterClosing[0]);
+ markupSpans.Add((name, markupSpan));
+
+ remainingCode = midAndAfterClosing[1];
+ remainingCodeStart += beforeAndAfterOpening[0].Length + markupSpan.Length;
+ }
+
+ documents[i] = code.ToString();
+ }
+
+ return (documents, markupSpans);
+ }
+
+ protected static readonly bool VerboseLogging = Environment.GetEnvironmentVariable("CI") != "true"
+ || Environment.GetEnvironmentVariable("VERBOSE_TEST_LOGGING")?.ToLower() == "true";
+ }
+}
diff --git a/src/NServiceBus.AzureFunctions.Analyzer.Tests/ConfigurationAnalyzerTests.cs b/src/NServiceBus.AzureFunctions.Analyzer.Tests/ConfigurationAnalyzerTests.cs
new file mode 100644
index 00000000..ffca45aa
--- /dev/null
+++ b/src/NServiceBus.AzureFunctions.Analyzer.Tests/ConfigurationAnalyzerTests.cs
@@ -0,0 +1,74 @@
+namespace NServiceBus.AzureFunctions.Analyzer.Tests
+{
+ using System.Threading.Tasks;
+ using NUnit.Framework;
+ using static AzureFunctionsDiagnostics;
+
+ [TestFixture]
+ public class ConfigurationAnalyzerTests : AnalyzerTestFixture
+ {
+ [TestCase("DefineCriticalErrorAction((errorContext, cancellationToken) => Task.CompletedTask)", DefineCriticalErrorActionNotAllowedId)]
+ [TestCase("LimitMessageProcessingConcurrencyTo(5)", LimitMessageProcessingToNotAllowedId)]
+ [TestCase("MakeInstanceUniquelyAddressable(null)", MakeInstanceUniquelyAddressableNotAllowedId)]
+ [TestCase("OverrideLocalAddress(null)", OverrideLocalAddressNotAllowedId)]
+ [TestCase("PurgeOnStartup(true)", PurgeOnStartupNotAllowedId)]
+ [TestCase("SetDiagnosticsPath(null)", SetDiagnosticsPathNotAllowedId)]
+ [TestCase("UseTransport(new AzureServiceBusTransport(null))", UseTransportNotAllowedId)]
+ public Task DiagnosticIsReportedForEndpointConfiguration(string configuration, string diagnosticId)
+ {
+ var source =
+ $@"using NServiceBus;
+using System;
+using System.Threading.Tasks;
+class Foo
+{{
+ void Bar(ServiceBusTriggeredEndpointConfiguration endpointConfig)
+ {{
+ [|endpointConfig.AdvancedConfiguration.{configuration}|];
+
+ var advancedConfig = endpointConfig.AdvancedConfiguration;
+ [|advancedConfig.{configuration}|];
+ }}
+}}";
+
+ return Assert(diagnosticId, source);
+ }
+
+ [TestCase("DefineCriticalErrorAction((errorContext, cancellationToken) => Task.CompletedTask)", DefineCriticalErrorActionNotAllowedId)]
+ [TestCase("LimitMessageProcessingConcurrencyTo(5)", LimitMessageProcessingToNotAllowedId)]
+ [TestCase("MakeInstanceUniquelyAddressable(null)", MakeInstanceUniquelyAddressableNotAllowedId)]
+ [TestCase("OverrideLocalAddress(null)", OverrideLocalAddressNotAllowedId)]
+ [TestCase("PurgeOnStartup(true)", PurgeOnStartupNotAllowedId)]
+ [TestCase("SetDiagnosticsPath(null)", SetDiagnosticsPathNotAllowedId)]
+ [TestCase("UseTransport(new AzureServiceBusTransport(null))", UseTransportNotAllowedId)]
+ public Task DiagnosticIsNotReportedForOtherEndpointConfiguration(string configuration, string diagnosticId)
+ {
+ var source =
+ $@"using NServiceBus;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+class SomeOtherClass
+{{
+ internal void DefineCriticalErrorAction(Func onCriticalError) {{ }}
+ internal void LimitMessageProcessingConcurrencyTo(int Number) {{ }}
+ internal void MakeInstanceUniquelyAddressable(string someProperty) {{ }}
+ internal void OverrideLocalAddress(string someProperty) {{ }}
+ internal void PurgeOnStartup(bool purge) {{ }}
+ internal void SetDiagnosticsPath(string someProperty) {{ }}
+ internal void UseTransport(AzureServiceBusTransport transport) {{ }}
+}}
+
+class Foo
+{{
+ void Bar(SomeOtherClass endpointConfig)
+ {{
+ endpointConfig.{configuration};
+ }}
+}}";
+
+ return Assert(diagnosticId, source);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NServiceBus.AzureFunctions.Analyzer.Tests/ConfigurationAnalyzerTestsCSharp8.cs b/src/NServiceBus.AzureFunctions.Analyzer.Tests/ConfigurationAnalyzerTestsCSharp8.cs
new file mode 100644
index 00000000..cce95f33
--- /dev/null
+++ b/src/NServiceBus.AzureFunctions.Analyzer.Tests/ConfigurationAnalyzerTestsCSharp8.cs
@@ -0,0 +1,34 @@
+namespace NServiceBus.AzureFunctions.Analyzer.Tests
+{
+ using System.Threading.Tasks;
+ using Microsoft.CodeAnalysis.CSharp;
+ using NUnit.Framework;
+ using static AzureFunctionsDiagnostics;
+
+ [TestFixture]
+ public class ConfigurationAnalyzerTestsCSharp8 : AnalyzerTestFixture
+ {
+ // HINT: In C# 7 this call is ambiguous with the LearningTransport version as the compiler cannot differentiate method calls via generic type constraints
+ [TestCase("UseTransport()", UseTransportNotAllowedId)]
+ public Task DiagnosticIsReportedForEndpointConfiguration(string configuration, string diagnosticId)
+ {
+ var source =
+ $@"using NServiceBus;
+using System;
+using System.Threading.Tasks;
+class Foo
+{{
+ void Bar(ServiceBusTriggeredEndpointConfiguration endpointConfig)
+ {{
+ [|endpointConfig.AdvancedConfiguration.{configuration}|];
+
+ var advancedConfig = endpointConfig.AdvancedConfiguration;
+ [|advancedConfig.{configuration}|];
+ }}
+}}";
+
+ return Assert(diagnosticId, source);
+ }
+ protected override LanguageVersion AnalyzerLanguageVersion => LanguageVersion.CSharp8;
+ }
+}
\ No newline at end of file
diff --git a/src/NServiceBus.AzureFunctions.Analyzer.Tests/Extensions/CompilationExtensions.cs b/src/NServiceBus.AzureFunctions.Analyzer.Tests/Extensions/CompilationExtensions.cs
new file mode 100644
index 00000000..45086c62
--- /dev/null
+++ b/src/NServiceBus.AzureFunctions.Analyzer.Tests/Extensions/CompilationExtensions.cs
@@ -0,0 +1,60 @@
+namespace NServiceBus.AzureFunctions.Analyzer.Tests
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Collections.Immutable;
+ using System.Diagnostics;
+ using System.IO;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.CodeAnalysis;
+ using Microsoft.CodeAnalysis.Diagnostics;
+
+ static class CompilationExtensions
+ {
+ public static void Compile(this Compilation compilation, bool throwOnFailure = true)
+ {
+ using (var peStream = new MemoryStream())
+ {
+ var emitResult = compilation.Emit(peStream);
+
+ if (!emitResult.Success)
+ {
+ if (throwOnFailure)
+ {
+ throw new Exception("Compilation failed.");
+ }
+ else
+ {
+ Debug.WriteLine("Compilation failed.");
+ }
+ }
+ }
+ }
+
+ public static async Task> GetAnalyzerDiagnostics(this Compilation compilation, DiagnosticAnalyzer analyzer, CancellationToken cancellationToken = default)
+ {
+ var exceptions = new List();
+
+ var analysisOptions = new CompilationWithAnalyzersOptions(
+ new AnalyzerOptions(ImmutableArray.Empty),
+ (exception, _, __) => exceptions.Add(exception),
+ concurrentAnalysis: false,
+ logAnalyzerExecutionTime: false);
+
+ var diagnostics = await compilation
+ .WithAnalyzers(ImmutableArray.Create(analyzer), analysisOptions)
+ .GetAnalyzerDiagnosticsAsync(cancellationToken);
+
+ if (exceptions.Any())
+ {
+ throw new AggregateException(exceptions);
+ }
+
+ return diagnostics
+ .OrderBy(diagnostic => diagnostic.Location.SourceSpan)
+ .ThenBy(diagnostic => diagnostic.Id);
+ }
+ }
+}
diff --git a/src/NServiceBus.AzureFunctions.Analyzer.Tests/Extensions/DocumentExtensions.cs b/src/NServiceBus.AzureFunctions.Analyzer.Tests/Extensions/DocumentExtensions.cs
new file mode 100644
index 00000000..b02a6207
--- /dev/null
+++ b/src/NServiceBus.AzureFunctions.Analyzer.Tests/Extensions/DocumentExtensions.cs
@@ -0,0 +1,48 @@
+namespace NServiceBus.AzureFunctions.Analyzer.Tests
+{
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.CodeAnalysis;
+ using Microsoft.CodeAnalysis.CodeActions;
+ using Microsoft.CodeAnalysis.CodeFixes;
+ using Microsoft.CodeAnalysis.Formatting;
+ using Microsoft.CodeAnalysis.Simplification;
+
+ static class DocumentExtensions
+ {
+ public static async Task> GetCompilerDiagnostics(this Document document, CancellationToken cancellationToken = default) =>
+ (await document.GetSemanticModelAsync(cancellationToken))
+ .GetDiagnostics(cancellationToken: cancellationToken)
+ .Where(diagnostic => diagnostic.Severity != DiagnosticSeverity.Hidden)
+ .OrderBy(diagnostic => diagnostic.Location.SourceSpan)
+ .ThenBy(diagnostic => diagnostic.Id);
+
+ public static async Task<(Document Document, CodeAction Action)[]> GetCodeActions(this Project project, CodeFixProvider codeFix, Diagnostic diagnostic, CancellationToken cancellationToken = default)
+ {
+ var actions = new List<(Document, CodeAction)>();
+ foreach (var document in project.Documents)
+ {
+ var context = new CodeFixContext(document, diagnostic, (action, _) => actions.Add((document, action)), cancellationToken);
+ await codeFix.RegisterCodeFixesAsync(context);
+ }
+ return actions.ToArray();
+ }
+
+ public static async Task ApplyChanges(this Document document, CodeAction codeAction, CancellationToken cancellationToken = default)
+ {
+ var operations = await codeAction.GetOperationsAsync(cancellationToken);
+ var solution = operations.OfType().Single().ChangedSolution;
+ return solution.GetDocument(document.Id);
+ }
+
+ public static async Task GetCode(this Document document, CancellationToken cancellationToken = default)
+ {
+ var simplifiedDoc = await Simplifier.ReduceAsync(document, Simplifier.Annotation, cancellationToken: cancellationToken);
+ var root = await simplifiedDoc.GetSyntaxRootAsync(cancellationToken);
+ root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace, cancellationToken: cancellationToken);
+ return root.GetText().ToString();
+ }
+ }
+}
diff --git a/src/NServiceBus.AzureFunctions.Analyzer.Tests/GlobalSuppressions.cs b/src/NServiceBus.AzureFunctions.Analyzer.Tests/GlobalSuppressions.cs
new file mode 100644
index 00000000..e3d15703
--- /dev/null
+++ b/src/NServiceBus.AzureFunctions.Analyzer.Tests/GlobalSuppressions.cs
@@ -0,0 +1,11 @@
+// This file is used by Code Analysis to maintain SuppressMessage
+// attributes that are applied to this project.
+// Project-level suppressions either have no target or are given
+// a specific target and scoped to a namespace, type, member, etc.
+
+using System.Diagnostics.CodeAnalysis;
+
+[assembly: SuppressMessage("Style", "IDE0078:Use pattern matching (may change code meaning)", Justification = "")]
+[assembly: SuppressMessage("Style", "IDE0083:Use pattern matching", Justification = "")]
+[assembly: SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "")]
+[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression", Justification = "")]
diff --git a/src/NServiceBus.AzureFunctions.Analyzer.Tests/NServiceBus.AzureFunctions.Analyzer.Tests.csproj b/src/NServiceBus.AzureFunctions.Analyzer.Tests/NServiceBus.AzureFunctions.Analyzer.Tests.csproj
new file mode 100644
index 00000000..46cbf234
--- /dev/null
+++ b/src/NServiceBus.AzureFunctions.Analyzer.Tests/NServiceBus.AzureFunctions.Analyzer.Tests.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net6.0;net7.0
+ true
+ ..\NServiceBusTests.snk
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/NServiceBus.AzureFunctions.Analyzer.Tests/OptionsAnalyzerTests.cs b/src/NServiceBus.AzureFunctions.Analyzer.Tests/OptionsAnalyzerTests.cs
new file mode 100644
index 00000000..8ffc2493
--- /dev/null
+++ b/src/NServiceBus.AzureFunctions.Analyzer.Tests/OptionsAnalyzerTests.cs
@@ -0,0 +1,55 @@
+namespace NServiceBus.AzureFunctions.Analyzer.Tests
+{
+ using System.Threading.Tasks;
+ using NUnit.Framework;
+ using static AzureFunctionsDiagnostics;
+
+ [TestFixture]
+ public class OptionsAnalyzerTests : AnalyzerTestFixture
+ {
+ [TestCase("SendOptions", "RouteReplyToThisInstance", RouteReplyToThisInstanceNotAllowedId)]
+ [TestCase("SendOptions", "RouteToThisInstance", RouteToThisInstanceNotAllowedId)]
+ [TestCase("ReplyOptions", "RouteReplyToThisInstance", RouteReplyToThisInstanceNotAllowedId)]
+ public Task DiagnosticIsReportedForOptions(string optionsType, string method, string diagnosticId)
+ {
+ var source =
+ $@"using NServiceBus;
+class Foo
+{{
+ void Bar({optionsType} options)
+ {{
+ [|options.{method}()|];
+ }}
+}}";
+
+ return Assert(diagnosticId, source);
+ }
+
+ [TestCase("SomeOtherClass", "RouteReplyToThisInstance", RouteReplyToThisInstanceNotAllowedId)]
+ [TestCase("SomeOtherClass", "RouteToThisInstance", RouteToThisInstanceNotAllowedId)]
+ public Task DiagnosticIsNotReportedForOtherOptions(string optionsType, string method, string diagnosticId)
+ {
+ var source =
+ $@"using NServiceBus;
+using System;
+using System.Threading.Tasks;
+
+class SomeOtherClass
+{{
+ internal void RouteReplyToAnyInstance() {{ }}
+ internal void RouteReplyToThisInstance() {{ }}
+ internal void RouteToThisInstance() {{ }}
+}}
+
+class Foo
+{{
+ void Bar({optionsType} options)
+ {{
+ options.{method}();
+ }}
+}}";
+
+ return Assert(diagnosticId, source);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NServiceBus.AzureFunctions.Analyzer.Tests/TransportConfigurationAnalyzerTests.cs b/src/NServiceBus.AzureFunctions.Analyzer.Tests/TransportConfigurationAnalyzerTests.cs
new file mode 100644
index 00000000..264709b2
--- /dev/null
+++ b/src/NServiceBus.AzureFunctions.Analyzer.Tests/TransportConfigurationAnalyzerTests.cs
@@ -0,0 +1,95 @@
+namespace NServiceBus.AzureFunctions.Analyzer.Tests
+{
+ using System.Threading.Tasks;
+ using NUnit.Framework;
+ using static AzureFunctionsDiagnostics;
+
+ [TestFixture]
+ public class TransportConfigurationAnalyzerTests : AnalyzerTestFixture
+ {
+ [TestCase("TransportTransactionMode", "TransportTransactionMode.None", TransportTransactionModeNotAllowedId)]
+ [TestCase("EnablePartitioning", "true", EnablePartitioningNotAllowedId)]
+ [TestCase("EntityMaximumSize", "5", EntityMaximumSizeNotAllowedId)]
+ [TestCase("MaxAutoLockRenewalDuration", "new System.TimeSpan(0, 0, 5, 0)", MaxAutoLockRenewalDurationNotAllowedId)]
+ [TestCase("PrefetchCount", "5", PrefetchCountNotAllowedId)]
+ [TestCase("PrefetchMultiplier", "5", PrefetchMultiplierNotAllowedId)]
+ [TestCase("TimeToWaitBeforeTriggeringCircuitBreaker", "new System.TimeSpan(0, 0, 5, 0)", TimeToWaitBeforeTriggeringCircuitBreakerNotAllowedId)]
+ public Task DiagnosticIsReportedTransportConfigurationDirect(string configName, string configValue, string diagnosticId)
+ {
+ var source =
+ $@"using NServiceBus;
+using System;
+using System.Threading.Tasks;
+class Foo
+{{
+ void Direct(ServiceBusTriggeredEndpointConfiguration endpointConfig)
+ {{
+ [|endpointConfig.Transport.{configName}|] = {configValue};
+
+ var transportConfig = endpointConfig.Transport;
+ [|transportConfig.{configName}|] = {configValue};
+ }}
+}}";
+
+ return Assert(diagnosticId, source);
+ }
+
+ [TestCase("Transactions", "TransportTransactionMode.None", TransportTransactionModeNotAllowedId)]
+ [TestCase("EnablePartitioning", "", EnablePartitioningNotAllowedId)]
+ [TestCase("EntityMaximumSize", "5", EntityMaximumSizeNotAllowedId)]
+ [TestCase("MaxAutoLockRenewalDuration", "new System.TimeSpan(0, 0, 5, 0)", MaxAutoLockRenewalDurationNotAllowedId)]
+ [TestCase("PrefetchCount", "5", PrefetchCountNotAllowedId)]
+ [TestCase("PrefetchMultiplier", "5", PrefetchMultiplierNotAllowedId)]
+ [TestCase("TimeToWaitBeforeTriggeringCircuitBreaker", "new System.TimeSpan(0, 0, 5, 0)", TimeToWaitBeforeTriggeringCircuitBreakerNotAllowedId)]
+ public Task DiagnosticIsReportedTransportConfigurationExtension(string configName, string configValue, string diagnosticId)
+ {
+ var source =
+ $@"using NServiceBus;
+using System;
+using System.Threading.Tasks;
+class Foo
+{{
+ void Extension(TransportExtensions transportExtension)
+ {{
+ [|transportExtension.{configName}({configValue})|];
+ }}
+}}";
+
+ return Assert(diagnosticId, source);
+ }
+
+ [TestCase("EnablePartitioning", "true", EnablePartitioningNotAllowedId)]
+ [TestCase("EntityMaximumSize", "5", EntityMaximumSizeNotAllowedId)]
+ [TestCase("MaxAutoLockRenewalDuration", "new System.TimeSpan(0, 0, 5, 0)", MaxAutoLockRenewalDurationNotAllowedId)]
+ [TestCase("PrefetchCount", "5", PrefetchCountNotAllowedId)]
+ [TestCase("PrefetchMultiplier", "5", PrefetchMultiplierNotAllowedId)]
+ [TestCase("TimeToWaitBeforeTriggeringCircuitBreaker", "new System.TimeSpan(0, 0, 5, 0)", TimeToWaitBeforeTriggeringCircuitBreakerNotAllowedId)]
+ public Task DiagnosticIsNotReportedForNonTransportConfiguration(string configName, string configValue, string diagnosticId)
+ {
+ var source =
+ $@"using NServiceBus;
+using System;
+using System.Threading.Tasks;
+
+class SomeOtherClass
+{{
+ internal int EntityMaximumSize {{ get; set; }}
+ internal bool EnablePartitioning {{ get; set; }}
+ internal TimeSpan MaxAutoLockRenewalDuration {{ get; set; }}
+ internal int PrefetchCount {{ get; set; }}
+ internal int PrefetchMultiplier {{ get; set; }}
+ internal TimeSpan TimeToWaitBeforeTriggeringCircuitBreaker {{ get; set; }}
+}}
+
+class Foo
+{{
+ void Direct(SomeOtherClass endpointConfig)
+ {{
+ endpointConfig.{configName} = {configValue};
+ }}
+}}";
+
+ return Assert(diagnosticId, source);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NServiceBus.AzureFunctions.Analyzer/.editorconfig b/src/NServiceBus.AzureFunctions.Analyzer/.editorconfig
new file mode 100644
index 00000000..dcd1e4a4
--- /dev/null
+++ b/src/NServiceBus.AzureFunctions.Analyzer/.editorconfig
@@ -0,0 +1,4 @@
+[*.cs]
+
+dotnet_diagnostic.RS2008.severity = none # Enable analyzer release tracking
+dotnet_diagnostic.RS1032.severity = none # Diagnostic message sentence structure
\ No newline at end of file
diff --git a/src/NServiceBus.AzureFunctions.Analyzer/AzureFunctionsDiagnostics.cs b/src/NServiceBus.AzureFunctions.Analyzer/AzureFunctionsDiagnostics.cs
new file mode 100644
index 00000000..9d944d6f
--- /dev/null
+++ b/src/NServiceBus.AzureFunctions.Analyzer/AzureFunctionsDiagnostics.cs
@@ -0,0 +1,171 @@
+namespace NServiceBus.AzureFunctions.Analyzer
+{
+ using Microsoft.CodeAnalysis;
+
+ public static class AzureFunctionsDiagnostics
+ {
+ public const string PurgeOnStartupNotAllowedId = "NSBFUNC003";
+ public const string LimitMessageProcessingToNotAllowedId = "NSBFUNC004";
+ public const string DefineCriticalErrorActionNotAllowedId = "NSBFUNC005";
+ public const string SetDiagnosticsPathNotAllowedId = "NSBFUNC006";
+ public const string MakeInstanceUniquelyAddressableNotAllowedId = "NSBFUNC007";
+ public const string UseTransportNotAllowedId = "NSBFUNC008";
+ public const string OverrideLocalAddressNotAllowedId = "NSBFUNC009";
+ public const string RouteReplyToThisInstanceNotAllowedId = "NSBFUNC010";
+ public const string RouteToThisInstanceNotAllowedId = "NSBFUNC011";
+ public const string TransportTransactionModeNotAllowedId = "NSBFUNC012";
+ public const string MaxAutoLockRenewalDurationNotAllowedId = "NSBFUNC013";
+ public const string PrefetchCountNotAllowedId = "NSBFUNC014";
+ public const string PrefetchMultiplierNotAllowedId = "NSBFUNC015";
+ public const string TimeToWaitBeforeTriggeringCircuitBreakerNotAllowedId = "NSBFUNC016";
+
+ public const string EntityMaximumSizeNotAllowedId = "NSBFUNC017";
+ public const string EnablePartitioningNotAllowedId = "NSBFUNC018";
+
+ const string DiagnosticCategory = "NServiceBus.AzureFunctions";
+
+ internal static readonly DiagnosticDescriptor PurgeOnStartupNotAllowed = new DiagnosticDescriptor(
+ id: PurgeOnStartupNotAllowedId,
+ title: "PurgeOnStartup is not supported in Azure Functions",
+ messageFormat: "Azure Functions endpoints do not support PurgeOnStartup.",
+ category: DiagnosticCategory,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ internal static readonly DiagnosticDescriptor LimitMessageProcessingToNotAllowed = new DiagnosticDescriptor(
+ id: LimitMessageProcessingToNotAllowedId,
+ title: "LimitMessageProcessing is not supported in Azure Functions",
+ messageFormat: "Concurrency-related settings are controlled via the Azure Function host.json configuration file.",
+ category: DiagnosticCategory,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ internal static readonly DiagnosticDescriptor DefineCriticalErrorActionNotAllowed = new DiagnosticDescriptor(
+ id: DefineCriticalErrorActionNotAllowedId,
+ title: "DefineCriticalErrorAction is not supported in Azure Functions",
+ messageFormat: "Azure Functions endpoints do not control the application lifecycle and should not define behavior in the case of critical errors.",
+ category: DiagnosticCategory,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ internal static readonly DiagnosticDescriptor SetDiagnosticsPathNotAllowed = new DiagnosticDescriptor(
+ id: SetDiagnosticsPathNotAllowedId,
+ title: "SetDiagnosticsPath is not supported in Azure Functions",
+ messageFormat: "Azure Functions endpoints should not write diagnostics to the local file system. Use CustomDiagnosticsWriter to write diagnostics to another location.",
+ category: DiagnosticCategory,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ internal static readonly DiagnosticDescriptor MakeInstanceUniquelyAddressableNotAllowed = new DiagnosticDescriptor(
+ id: MakeInstanceUniquelyAddressableNotAllowedId,
+ title: "MakeInstanceUniquelyAddressable is not supported in Azure Functions",
+ messageFormat: "Azure Functions endpoints have unpredictable lifecycles and should not be uniquely addressable.",
+ category: DiagnosticCategory,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ internal static readonly DiagnosticDescriptor UseTransportNotAllowed = new DiagnosticDescriptor(
+ id: UseTransportNotAllowedId,
+ title: "UseTransport is not supported in Azure Functions",
+ messageFormat: "The package configures Azure Service Bus transport by default. Use ServiceBusTriggeredEndpointConfiguration.Transport to access the transport configuration.",
+ category: DiagnosticCategory,
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
+
+ internal static readonly DiagnosticDescriptor OverrideLocalAddressNotAllowed = new DiagnosticDescriptor(
+ id: OverrideLocalAddressNotAllowedId,
+ title: "OverrideLocalAddress is not supported in Azure Functions",
+ messageFormat: "The NServiceBus endpoint address in Azure Functions is determined by the ServiceBusTrigger attribute.",
+ category: DiagnosticCategory,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ internal static readonly DiagnosticDescriptor RouteReplyToThisInstanceNotAllowed = new DiagnosticDescriptor(
+ id: RouteReplyToThisInstanceNotAllowedId,
+ title: "RouteReplyToThisInstance is not supported in Azure Functions",
+ messageFormat: "Azure Functions instances cannot be directly addressed as they have a highly volatile lifetime. Use 'RouteToThisEndpoint' instead.",
+ category: DiagnosticCategory,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ internal static readonly DiagnosticDescriptor RouteToThisInstanceNotAllowed = new DiagnosticDescriptor(
+ id: RouteToThisInstanceNotAllowedId,
+ title: "RouteToThisInstance is not supported in Azure Functions",
+ messageFormat: "Azure Functions instances cannot be directly addressed as they have a highly volatile lifetime. Use 'RouteToThisEndpoint' instead.",
+ category: DiagnosticCategory,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ internal static readonly DiagnosticDescriptor MaxAutoLockRenewalDurationNotAllowed = new DiagnosticDescriptor(
+ id: MaxAutoLockRenewalDurationNotAllowedId,
+ title: "MaxAutoLockRenewalDuration is not supported in Azure Functions",
+ messageFormat: "Azure Functions endpoints do not control the message receiver and cannot decide the lock renewal duration.",
+ category: DiagnosticCategory,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ internal static readonly DiagnosticDescriptor PrefetchCountNotAllowed = new DiagnosticDescriptor(
+ id: PrefetchCountNotAllowedId,
+ title: "PrefetchCount is not supported in Azure Functions",
+ messageFormat: "Message prefetching is controlled by the Azure Service Bus trigger and cannot be configured via the NServiceBus transport configuration API when using Azure Functions.",
+ category: DiagnosticCategory,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ internal static readonly DiagnosticDescriptor PrefetchMultiplierNotAllowed = new DiagnosticDescriptor(
+ id: PrefetchMultiplierNotAllowedId,
+ title: "PrefetchMultiplier is not supported in Azure Functions",
+ messageFormat: "Message prefetching is controlled by the Azure Service Bus trigger and cannot be configured via the NServiceBus transport configuration API when using Azure Functions",
+ category: DiagnosticCategory,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ internal static readonly DiagnosticDescriptor TimeToWaitBeforeTriggeringCircuitBreakerNotAllowed = new DiagnosticDescriptor(
+ id: TimeToWaitBeforeTriggeringCircuitBreakerNotAllowedId,
+ title: "TimeToWaitBeforeTriggeringCircuitBreaker is not supported in Azure Functions",
+ messageFormat: "Azure Functions endpoints do not control the message receiver and cannot access circuit breaker settings.",
+ category: DiagnosticCategory,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ internal static readonly DiagnosticDescriptor EntityMaximumSizeNotAllowed = new DiagnosticDescriptor(
+ id: EntityMaximumSizeNotAllowedId,
+ title: "EntityMaximumSize is not supported in Azure Functions",
+ messageFormat: "Azure Functions endpoints do not support automatic queue creation.",
+ category: DiagnosticCategory,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ internal static readonly DiagnosticDescriptor EnablePartitioningNotAllowed = new DiagnosticDescriptor(
+ id: EnablePartitioningNotAllowedId,
+ title: "EnablePartitioning is not supported in Azure Functions",
+ messageFormat: "Azure Functions endpoints do not support automatic queue creation.",
+ category: DiagnosticCategory,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ internal static readonly DiagnosticDescriptor TransportTransactionModeNotAllowed = new DiagnosticDescriptor(
+ id: TransportTransactionModeNotAllowedId,
+ title: "TransportTransactionMode is not supported in Azure Functions",
+ messageFormat: "Transport TransactionMode is controlled by the Azure Service Bus trigger and cannot be configured via the NServiceBus transport configuration API when using Azure Functions.",
+ category: DiagnosticCategory,
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs b/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs
new file mode 100644
index 00000000..1c8b1ab6
--- /dev/null
+++ b/src/NServiceBus.AzureFunctions.Analyzer/ConfigurationAnalyzer.cs
@@ -0,0 +1,179 @@
+namespace NServiceBus.AzureFunctions.Analyzer
+{
+ using System.Collections.Generic;
+ using System.Collections.Immutable;
+ using Microsoft.CodeAnalysis;
+ using Microsoft.CodeAnalysis.CSharp;
+ using Microsoft.CodeAnalysis.CSharp.Syntax;
+ using Microsoft.CodeAnalysis.Diagnostics;
+ using NServiceBus.AzureFunctions.Analyzer.Extensions;
+
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class ConfigurationAnalyzer : DiagnosticAnalyzer
+ {
+ public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(
+ AzureFunctionsDiagnostics.PurgeOnStartupNotAllowed,
+ AzureFunctionsDiagnostics.LimitMessageProcessingToNotAllowed,
+ AzureFunctionsDiagnostics.DefineCriticalErrorActionNotAllowed,
+ AzureFunctionsDiagnostics.SetDiagnosticsPathNotAllowed,
+ AzureFunctionsDiagnostics.MakeInstanceUniquelyAddressableNotAllowed,
+ AzureFunctionsDiagnostics.UseTransportNotAllowed,
+ AzureFunctionsDiagnostics.OverrideLocalAddressNotAllowed,
+ AzureFunctionsDiagnostics.RouteReplyToThisInstanceNotAllowed,
+ AzureFunctionsDiagnostics.RouteToThisInstanceNotAllowed,
+ AzureFunctionsDiagnostics.MaxAutoLockRenewalDurationNotAllowed,
+ AzureFunctionsDiagnostics.PrefetchCountNotAllowed,
+ AzureFunctionsDiagnostics.PrefetchMultiplierNotAllowed,
+ AzureFunctionsDiagnostics.TimeToWaitBeforeTriggeringCircuitBreakerNotAllowed,
+ AzureFunctionsDiagnostics.EntityMaximumSizeNotAllowed,
+ AzureFunctionsDiagnostics.EnablePartitioningNotAllowed,
+ AzureFunctionsDiagnostics.TransportTransactionModeNotAllowed
+ );
+
+ static readonly Dictionary NotAllowedEndpointConfigurationMethods
+ = new Dictionary
+ {
+ ["PurgeOnStartup"] = AzureFunctionsDiagnostics.PurgeOnStartupNotAllowed,
+ ["LimitMessageProcessingConcurrencyTo"] = AzureFunctionsDiagnostics.LimitMessageProcessingToNotAllowed,
+ ["DefineCriticalErrorAction"] = AzureFunctionsDiagnostics.DefineCriticalErrorActionNotAllowed,
+ ["SetDiagnosticsPath"] = AzureFunctionsDiagnostics.SetDiagnosticsPathNotAllowed,
+ ["MakeInstanceUniquelyAddressable"] = AzureFunctionsDiagnostics.MakeInstanceUniquelyAddressableNotAllowed,
+ ["UseTransport"] = AzureFunctionsDiagnostics.UseTransportNotAllowed,
+ ["OverrideLocalAddress"] = AzureFunctionsDiagnostics.OverrideLocalAddressNotAllowed,
+ };
+
+ static readonly Dictionary NotAllowedSendAndReplyOptions
+ = new Dictionary
+ {
+ ["RouteReplyToThisInstance"] = AzureFunctionsDiagnostics.RouteReplyToThisInstanceNotAllowed,
+ ["RouteToThisInstance"] = AzureFunctionsDiagnostics.RouteToThisInstanceNotAllowed,
+ };
+
+ static readonly Dictionary NotAllowedTransportSettings
+ = new Dictionary
+ {
+ ["MaxAutoLockRenewalDuration"] = AzureFunctionsDiagnostics.MaxAutoLockRenewalDurationNotAllowed,
+ ["PrefetchCount"] = AzureFunctionsDiagnostics.PrefetchCountNotAllowed,
+ ["PrefetchMultiplier"] = AzureFunctionsDiagnostics.PrefetchMultiplierNotAllowed,
+ ["TimeToWaitBeforeTriggeringCircuitBreaker"] = AzureFunctionsDiagnostics.TimeToWaitBeforeTriggeringCircuitBreakerNotAllowed,
+ ["EntityMaximumSize"] = AzureFunctionsDiagnostics.EntityMaximumSizeNotAllowed,
+ ["EnablePartitioning"] = AzureFunctionsDiagnostics.EnablePartitioningNotAllowed,
+ ["TransportTransactionMode"] = AzureFunctionsDiagnostics.TransportTransactionModeNotAllowed,
+ ["Transactions"] = AzureFunctionsDiagnostics.TransportTransactionModeNotAllowed
+ };
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.InvocationExpression);
+ context.RegisterSyntaxNodeAction(AnalyzeTransport, SyntaxKind.SimpleMemberAccessExpression);
+ }
+
+ static void Analyze(SyntaxNodeAnalysisContext context)
+ {
+ if (!(context.Node is InvocationExpressionSyntax invocationExpression))
+ {
+ return;
+ }
+
+ if (!(invocationExpression.Expression is MemberAccessExpressionSyntax memberAccessExpression))
+ {
+ return;
+ }
+
+ AnalyzeEndpointConfiguration(context, invocationExpression, memberAccessExpression);
+
+ AnalyzeSendAndReplyOptions(context, invocationExpression, memberAccessExpression);
+
+ AnalyzeTransportExtensions(context, invocationExpression, memberAccessExpression);
+ }
+
+ static void AnalyzeTransport(SyntaxNodeAnalysisContext context)
+ {
+ if (!(context.Node is MemberAccessExpressionSyntax memberAccess))
+ {
+ return;
+ }
+
+ if (!NotAllowedTransportSettings.TryGetValue(memberAccess.Name.ToString(), out var diagnosticDescriptor))
+ {
+ return;
+
+ }
+
+ var memberAccessSymbol = context.SemanticModel.GetSymbolInfo(memberAccess, context.CancellationToken);
+
+ if (!(memberAccessSymbol.Symbol is IPropertySymbol propertySymbol))
+ {
+ return;
+ }
+
+ if (propertySymbol.ContainingType.ToString() == "NServiceBus.AzureServiceBusTransport" || propertySymbol.ContainingType.ToString() == "NServiceBus.Transport.TransportDefinition")
+ {
+ context.ReportDiagnostic(diagnosticDescriptor, memberAccess);
+
+ }
+ }
+
+ static void AnalyzeEndpointConfiguration(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocationExpression, MemberAccessExpressionSyntax memberAccessExpression)
+ {
+ if (!NotAllowedEndpointConfigurationMethods.TryGetValue(memberAccessExpression.Name.Identifier.Text, out var diagnosticDescriptor))
+ {
+ return;
+ }
+
+ var memberAccessSymbol = context.SemanticModel.GetSymbolInfo(memberAccessExpression, context.CancellationToken);
+
+ if (!(memberAccessSymbol.Symbol is IMethodSymbol methodSymbol))
+ {
+ return;
+ }
+
+ if (methodSymbol.ReceiverType.ToString() == "NServiceBus.EndpointConfiguration")
+ {
+ context.ReportDiagnostic(diagnosticDescriptor, invocationExpression);
+ }
+ }
+
+ static void AnalyzeSendAndReplyOptions(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocationExpression, MemberAccessExpressionSyntax memberAccessExpression)
+ {
+ if (!NotAllowedSendAndReplyOptions.TryGetValue(memberAccessExpression.Name.Identifier.Text, out var diagnosticDescriptor))
+ {
+ return;
+ }
+
+ var memberAccessSymbol = context.SemanticModel.GetSymbolInfo(memberAccessExpression, context.CancellationToken);
+
+ if (!(memberAccessSymbol.Symbol is IMethodSymbol methodSymbol))
+ {
+ return;
+ }
+
+ if (methodSymbol.ReceiverType.ToString() == "NServiceBus.SendOptions" || methodSymbol.ReceiverType.ToString() == "NServiceBus.ReplyOptions")
+ {
+ context.ReportDiagnostic(diagnosticDescriptor, invocationExpression);
+ }
+ }
+
+ static void AnalyzeTransportExtensions(SyntaxNodeAnalysisContext context, InvocationExpressionSyntax invocationExpression, MemberAccessExpressionSyntax memberAccessExpression)
+ {
+ if (!NotAllowedTransportSettings.TryGetValue(memberAccessExpression.Name.Identifier.Text, out var diagnosticDescriptor))
+ {
+ return;
+ }
+
+ var memberAccessSymbol = context.SemanticModel.GetSymbolInfo(memberAccessExpression, context.CancellationToken);
+
+ if (!(memberAccessSymbol.Symbol is IMethodSymbol methodSymbol))
+ {
+ return;
+ }
+
+ if (methodSymbol.ReceiverType.ToString() == "NServiceBus.TransportExtensions")
+ {
+ context.ReportDiagnostic(diagnosticDescriptor, invocationExpression);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NServiceBus.AzureFunctions.Analyzer/Extensions/SyntaxNodeAnalysisContextExtension.cs b/src/NServiceBus.AzureFunctions.Analyzer/Extensions/SyntaxNodeAnalysisContextExtension.cs
new file mode 100644
index 00000000..99cc6529
--- /dev/null
+++ b/src/NServiceBus.AzureFunctions.Analyzer/Extensions/SyntaxNodeAnalysisContextExtension.cs
@@ -0,0 +1,25 @@
+namespace NServiceBus.AzureFunctions.Analyzer.Extensions
+{
+ using Microsoft.CodeAnalysis;
+ using Microsoft.CodeAnalysis.Diagnostics;
+
+ static class SyntaxNodeAnalysisContextExtension
+ {
+ public static void ReportDiagnostic(this SyntaxNodeAnalysisContext context, DiagnosticDescriptor descriptor, ISymbol symbol, params object[] messageArgs)
+ {
+ foreach (var location in symbol.Locations)
+ {
+ context.ReportDiagnostic(descriptor, location, messageArgs);
+ }
+ }
+
+ public static void ReportDiagnostic(this SyntaxNodeAnalysisContext context, DiagnosticDescriptor descriptor, SyntaxNode node, params object[] messageArgs) =>
+ context.ReportDiagnostic(descriptor, node.GetLocation(), messageArgs);
+
+ public static void ReportDiagnostic(this SyntaxNodeAnalysisContext context, DiagnosticDescriptor descriptor, SyntaxToken token, params object[] messageArgs) =>
+ context.ReportDiagnostic(descriptor, token.GetLocation(), messageArgs);
+
+ public static void ReportDiagnostic(this SyntaxNodeAnalysisContext context, DiagnosticDescriptor descriptor, Location location, params object[] messageArgs) =>
+ context.ReportDiagnostic(Diagnostic.Create(descriptor, location, messageArgs));
+ }
+}
diff --git a/src/NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.csproj b/src/NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.csproj
new file mode 100644
index 00000000..3748f2a7
--- /dev/null
+++ b/src/NServiceBus.AzureFunctions.Analyzer/NServiceBus.AzureFunctions.Analyzer.csproj
@@ -0,0 +1,18 @@
+
+
+
+ $(AnalyzerTargetFramework)
+ true
+ true
+ ..\NServiceBus.snk
+ false
+ false
+
+
+
+
+
+
+
+
+
diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus.sln b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus.sln
index ed65f2f8..5a67fcd9 100644
--- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus.sln
+++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus.sln
@@ -23,6 +23,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceBus.AcceptanceTests"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests.HostV4", "IntegrationTests.HostV4\IntegrationTests.HostV4.csproj", "{D4B26C04-CD88-4356-922F-CCF69D74F442}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NServiceBus.AzureFunctions.Analyzer", "NServiceBus.AzureFunctions.Analyzer\NServiceBus.AzureFunctions.Analyzer.csproj", "{0D840DDA-A554-4764-871E-3ACB7A454FF9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NServiceBus.AzureFunctions.Analyzer.Tests", "NServiceBus.AzureFunctions.Analyzer.Tests\NServiceBus.AzureFunctions.Analyzer.Tests.csproj", "{BA6AF5D9-9784-498A-9E9E-8A726A35B0EE}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -57,6 +61,14 @@ Global
{D4B26C04-CD88-4356-922F-CCF69D74F442}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4B26C04-CD88-4356-922F-CCF69D74F442}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4B26C04-CD88-4356-922F-CCF69D74F442}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0D840DDA-A554-4764-871E-3ACB7A454FF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0D840DDA-A554-4764-871E-3ACB7A454FF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0D840DDA-A554-4764-871E-3ACB7A454FF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0D840DDA-A554-4764-871E-3ACB7A454FF9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BA6AF5D9-9784-498A-9E9E-8A726A35B0EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BA6AF5D9-9784-498A-9E9E-8A726A35B0EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BA6AF5D9-9784-498A-9E9E-8A726A35B0EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BA6AF5D9-9784-498A-9E9E-8A726A35B0EE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBus.AzureFunctions.InProcess.ServiceBus.csproj b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBus.AzureFunctions.InProcess.ServiceBus.csproj
index 30a7ef6a..526a117d 100644
--- a/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBus.AzureFunctions.InProcess.ServiceBus.csproj
+++ b/src/NServiceBus.AzureFunctions.InProcess.ServiceBus/NServiceBus.AzureFunctions.InProcess.ServiceBus.csproj
@@ -1,4 +1,4 @@
-
+
net6.0
@@ -10,6 +10,10 @@
+
+
+
+