-
Notifications
You must be signed in to change notification settings - Fork 0
Clarify SW103 migration with concrete examples and keyed-only registration guidance #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
cliedeman
merged 4 commits into
main
from
copilot/implement-analyzer-for-microsoft-extensions-azure
May 31, 2026
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
adb73e4
Add SW103 analyzer, tests, and migration docs
Copilot 53c068e
Finalize SW103 analyzer with passing tests and docs
Copilot c54136a
docs: add concrete migration code samples for SW103
Copilot ab62f46
docs: note preference for keyed singleton registrations
Copilot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
121 changes: 121 additions & 0 deletions
121
Stackworx.Analyzers.Tests/AvoidMicrosoftExtensionsAzureAnalyzerTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| namespace Stackworx.Analyzers.Tests; | ||
|
|
||
| using System.Threading.Tasks; | ||
| using Microsoft.CodeAnalysis.CSharp.Testing; | ||
| using Microsoft.CodeAnalysis.Testing; | ||
| using Xunit; | ||
| using Verifier = | ||
| Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< | ||
| Stackworx.Analyzers.AvoidMicrosoftExtensionsAzureAnalyzer, | ||
| Microsoft.CodeAnalysis.Testing.DefaultVerifier>; | ||
|
|
||
| public class AvoidMicrosoftExtensionsAzureAnalyzerTests | ||
| { | ||
| private const string FakeAzureNamespace = | ||
| """ | ||
| namespace Microsoft.Extensions.Azure | ||
| { | ||
| public sealed class AzureMarker { } | ||
| } | ||
| """; | ||
|
|
||
| private const string FakeServiceCollection = | ||
| """ | ||
| namespace Microsoft.Extensions.DependencyInjection | ||
| { | ||
| public interface IServiceCollection { } | ||
| public sealed class ServiceCollection : IServiceCollection { } | ||
| } | ||
| """; | ||
|
|
||
| private const string FakeAzureExtensions = | ||
| """ | ||
| namespace Microsoft.Extensions.Azure | ||
| { | ||
| using Microsoft.Extensions.DependencyInjection; | ||
|
|
||
| public static class AzureClientFactoryBuilderExtensions | ||
| { | ||
| public static IServiceCollection AddAzureClients(this IServiceCollection services) => services; | ||
| } | ||
| } | ||
| """; | ||
|
|
||
| [Fact] | ||
| public async Task Flags_UsingDirective_ForMicrosoftExtensionsAzure() | ||
| { | ||
| const string src = | ||
| """ | ||
| using {|#0:Microsoft.Extensions.Azure|}; | ||
|
|
||
| class C { } | ||
| """; | ||
|
|
||
| var test = new CSharpAnalyzerTest<Stackworx.Analyzers.AvoidMicrosoftExtensionsAzureAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier> | ||
| { | ||
| ReferenceAssemblies = ReferenceAssemblies.Net.Net80, | ||
| TestCode = src, | ||
| }; | ||
| test.TestState.Sources.Add(FakeAzureNamespace); | ||
| test.ExpectedDiagnostics.Add( | ||
| Verifier | ||
| .Diagnostic(Stackworx.Analyzers.AvoidMicrosoftExtensionsAzureAnalyzer.Rule) | ||
| .WithLocation(0)); | ||
|
|
||
| await test.RunAsync(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Flags_FullyQualified_Invocation_InForbiddenNamespace() | ||
| { | ||
| const string src = | ||
| """ | ||
| class C | ||
| { | ||
| void M() | ||
| { | ||
| Microsoft.Extensions.DependencyInjection.IServiceCollection services = new Microsoft.Extensions.DependencyInjection.ServiceCollection(); | ||
| {|#0:Microsoft.Extensions.Azure.AzureClientFactoryBuilderExtensions.AddAzureClients(services)|}; | ||
| } | ||
| } | ||
| """; | ||
|
|
||
| var test = new CSharpAnalyzerTest<Stackworx.Analyzers.AvoidMicrosoftExtensionsAzureAnalyzer, Microsoft.CodeAnalysis.Testing.DefaultVerifier> | ||
| { | ||
| ReferenceAssemblies = ReferenceAssemblies.Net.Net80, | ||
| TestCode = src, | ||
| }; | ||
| test.TestState.Sources.Add(FakeServiceCollection); | ||
| test.TestState.Sources.Add(FakeAzureExtensions); | ||
| test.ExpectedDiagnostics.Add( | ||
| Verifier | ||
| .Diagnostic(Stackworx.Analyzers.AvoidMicrosoftExtensionsAzureAnalyzer.Rule) | ||
| .WithLocation(0)); | ||
|
|
||
| await test.RunAsync(); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task DoesNotFlag_NearMatch_Namespace() | ||
| { | ||
| const string src = | ||
| """ | ||
| using Microsoft.Extensions.Azureish; | ||
|
|
||
| class C | ||
| { | ||
| void M() | ||
| { | ||
| var marker = new Microsoft.Extensions.Azureish.AzureMarker(); | ||
| } | ||
| } | ||
|
|
||
| namespace Microsoft.Extensions.Azureish | ||
| { | ||
| public sealed class AzureMarker { } | ||
| } | ||
| """; | ||
|
|
||
| await Verifier.VerifyAnalyzerAsync(src); | ||
| } | ||
| } |
81 changes: 81 additions & 0 deletions
81
Stackworx.Analyzers/AvoidMicrosoftExtensionsAzureAnalyzer.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| namespace Stackworx.Analyzers; | ||
|
|
||
| using System.Collections.Immutable; | ||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
| using Microsoft.CodeAnalysis.Diagnostics; | ||
| using Microsoft.CodeAnalysis.Operations; | ||
|
|
||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
| public sealed class AvoidMicrosoftExtensionsAzureAnalyzer : DiagnosticAnalyzer | ||
| { | ||
| public const string DiagnosticId = "SW103"; | ||
|
|
||
| private const string ForbiddenNamespace = "Microsoft.Extensions.Azure"; | ||
| private const string Category = "Architecture"; | ||
|
|
||
| private static readonly LocalizableString Title = | ||
| "Avoid Microsoft.Extensions.Azure usage"; | ||
|
|
||
| private static readonly LocalizableString MessageFormat = | ||
| "Usage of Microsoft.Extensions.Azure is discouraged; migrate to keyed service registrations"; | ||
|
|
||
| private static readonly LocalizableString Description = | ||
| "Microsoft.Extensions.Azure registrations should be replaced by keyed service registrations."; | ||
|
|
||
| public static readonly DiagnosticDescriptor Rule = new( | ||
| id: DiagnosticId, | ||
| title: Title, | ||
| messageFormat: MessageFormat, | ||
| category: Category, | ||
| defaultSeverity: DiagnosticSeverity.Warning, | ||
| isEnabledByDefault: true, | ||
| description: Description); | ||
|
|
||
| public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule); | ||
|
|
||
| public override void Initialize(AnalysisContext context) | ||
| { | ||
| context.EnableConcurrentExecution(); | ||
| context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); | ||
|
|
||
| context.RegisterSyntaxNodeAction(AnalyzeUsingDirective, Microsoft.CodeAnalysis.CSharp.SyntaxKind.UsingDirective); | ||
| context.RegisterOperationAction(AnalyzeOperation, OperationKind.Invocation, OperationKind.ObjectCreation); | ||
| } | ||
|
|
||
| private static void AnalyzeUsingDirective(SyntaxNodeAnalysisContext context) | ||
| { | ||
| var usingDirective = (UsingDirectiveSyntax)context.Node; | ||
| var namespaceText = usingDirective.Name?.ToString(); | ||
| if (namespaceText is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| if (namespaceText == ForbiddenNamespace || namespaceText.StartsWith(ForbiddenNamespace + ".", System.StringComparison.Ordinal)) | ||
| { | ||
| context.ReportDiagnostic(Diagnostic.Create(Rule, usingDirective.Name!.GetLocation())); | ||
| } | ||
| } | ||
|
|
||
| private static void AnalyzeOperation(OperationAnalysisContext context) | ||
| { | ||
| ISymbol? symbol = context.Operation switch | ||
| { | ||
| IInvocationOperation invocation => invocation.TargetMethod, | ||
| IObjectCreationOperation objectCreation => objectCreation.Constructor, | ||
| _ => null, | ||
| }; | ||
|
|
||
| if (symbol is null || symbol.ContainingNamespace is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| var namespaceText = symbol.ContainingNamespace.ToDisplayString(); | ||
| if (namespaceText == ForbiddenNamespace || namespaceText.StartsWith(ForbiddenNamespace + ".", System.StringComparison.Ordinal)) | ||
| { | ||
| context.ReportDiagnostic(Diagnostic.Create(Rule, context.Operation.Syntax.GetLocation())); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
docs/docs/rules/sw103-avoid-microsoft-extensions-azure.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| --- | ||
| sidebar_position: 8 | ||
| --- | ||
|
|
||
| # SW103: Avoid Microsoft.Extensions.Azure usage | ||
|
|
||
| ## Overview | ||
|
|
||
| **Rule ID:** SW103 | ||
| **Category:** Architecture | ||
| **Severity:** Warning | ||
| **Status:** Enabled by default | ||
|
|
||
| ## Description | ||
|
|
||
| This rule flags source usage of `Microsoft.Extensions.Azure` APIs (including `using Microsoft.Extensions.Azure;` and calls to symbols in that namespace). | ||
|
|
||
| The preferred direction is keyed services registration/resolution instead of `Microsoft.Extensions.Azure` builder-based registration. | ||
|
|
||
| ## When This Rule Triggers | ||
|
|
||
| The analyzer reports a diagnostic when: | ||
| - A `using` directive imports `Microsoft.Extensions.Azure` (or a child namespace) | ||
| - A method call or object creation targets a symbol in `Microsoft.Extensions.Azure` (or a child namespace) | ||
|
|
||
| ## Why This Matters | ||
|
|
||
| `Microsoft.Extensions.Azure` registration patterns (`AddAzureClients`, factory builders, etc.) make named registrations harder to reason about across large solutions. | ||
|
|
||
| Keyed services provide a clearer DI model where each Azure dependency is resolved with an explicit key and can be mapped directly to an Aspire resource binding. | ||
|
|
||
| ## Migration Notes (for automated and manual migrations) | ||
|
|
||
| Use this checklist when migrating code flagged by SW103: | ||
|
|
||
| 1. Identify `AddAzureClients(...)` blocks and collect each registered client name/key. | ||
| 2. Replace the `Microsoft.Extensions.Azure` registration with keyed DI registrations for the concrete Azure SDK client types. | ||
| 3. Keep one stable key per resource (for example, `"blob-storage"`), and use that same key at injection/resolution sites. | ||
| 4. Update constructors and call sites to resolve keyed services instead of `IAzureClientFactory<TClient>`. | ||
| 5. Remove `using Microsoft.Extensions.Azure;` once all references are migrated. | ||
| 6. Validate startup and integration tests to ensure each keyed binding resolves correctly. | ||
|
|
||
| ### Code samples | ||
|
|
||
| #### Registration — before (`AddAzureClients`) | ||
|
|
||
| ```csharp | ||
| // Program.cs / Startup.cs | ||
| using Microsoft.Extensions.Azure; | ||
|
|
||
| builder.Services.AddAzureClients(clients => | ||
| { | ||
| clients.AddBlobServiceClient(builder.Configuration.GetSection("AzureStorage:Blob")) | ||
| .WithName("blob-storage"); | ||
|
|
||
| clients.AddQueueServiceClient(builder.Configuration.GetSection("AzureStorage:Queue")) | ||
| .WithName("order-queue"); | ||
| }); | ||
| ``` | ||
|
|
||
| #### Registration — after (keyed services) | ||
|
|
||
| ```csharp | ||
| // Program.cs / Startup.cs | ||
| // No Microsoft.Extensions.Azure import required | ||
|
|
||
| builder.Services.AddKeyedSingleton<BlobServiceClient>("blob-storage", (sp, _) => | ||
| new BlobServiceClient( | ||
| builder.Configuration.GetConnectionString("BlobStorage"))); | ||
|
|
||
| builder.Services.AddKeyedSingleton<QueueServiceClient>("order-queue", (sp, _) => | ||
| new QueueServiceClient( | ||
| builder.Configuration.GetConnectionString("OrderQueue"))); | ||
| ``` | ||
|
|
||
| #### DI callsite — before (`IAzureClientFactory<T>`) | ||
|
|
||
| ```csharp | ||
| public class BlobUploadService | ||
| { | ||
| private readonly BlobServiceClient _blobClient; | ||
|
|
||
| public BlobUploadService(IAzureClientFactory<BlobServiceClient> factory) | ||
| { | ||
| _blobClient = factory.CreateClient("blob-storage"); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| #### DI callsite — after (keyed injection) | ||
|
|
||
| ```csharp | ||
| public class BlobUploadService | ||
| { | ||
| private readonly BlobServiceClient _blobClient; | ||
|
|
||
| public BlobUploadService( | ||
| [FromKeyedServices("blob-storage")] BlobServiceClient blobClient) | ||
| { | ||
| _blobClient = blobClient; | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Suggested review points after migration | ||
|
|
||
| - Every previous `name:`/`WithName(...)` registration has a corresponding keyed registration. | ||
| - Prefer keyed singleton registrations for Azure SDK clients; avoid adding a default unkeyed registration for the same client type. | ||
| - No remaining references to `Microsoft.Extensions.Azure` types (`IAzureClientFactory<>`, builder types, extension methods). | ||
| - Key names are centralized and reused consistently. | ||
| - Resource keys align with Aspire integration naming. | ||
|
|
||
| ## Related Documentation | ||
|
|
||
| - Aspire Azure Storage Blobs integration (keyed/resource-oriented approach): | ||
| https://aspire.dev/integrations/cloud/azure/azure-storage-blobs/azure-storage-blobs-connect/#blob-storage-resource | ||
| - Azure SDK discussion: | ||
| https://github.com/Azure/azure-sdk-for-net/issues/40408#issuecomment-3599496883 | ||
| - Azure SDK follow-up issue: | ||
| https://github.com/Azure/azure-sdk-for-net/issues/55491 | ||
|
|
||
| ## Configuration | ||
|
|
||
| To disable: | ||
|
|
||
| ```ini | ||
| [*.cs] | ||
| dotnet_diagnostic.SW103.severity = none | ||
| ``` | ||
|
|
||
| To enforce as error: | ||
|
|
||
| ```ini | ||
| [*.cs] | ||
| dotnet_diagnostic.SW103.severity = error | ||
| ``` | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add some migration samples to the repo.
DI callsites and AddAzureClients config
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done — added four concrete code samples to the Migration Notes section:
AddAzureClientsblock with two named clients (blob-storage,order-queue) and configuration sections.AddKeyedSingletonregistrations using connection strings, noMicrosoft.Extensions.Azureimport.IAzureClientFactory<BlobServiceClient>+CreateClient("blob-storage").[FromKeyedServices("blob-storage")]attribute.