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 @@ + + + +