Skip to content

MA0187: Migrate Blazor [Inject] property injection to constructor injection#1048

Merged
meziantou merged 5 commits intomainfrom
copilot/migrate-property-injection-to-constructor
Feb 27, 2026
Merged

MA0187: Migrate Blazor [Inject] property injection to constructor injection#1048
meziantou merged 5 commits intomainfrom
copilot/migrate-property-injection-to-constructor

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 27, 2026

  • Add IsCSharp12OrAbove to LanguageVersionExtensions.cs
  • Add MA0187 constant to RuleIdentifiers.cs
  • Add AspNetCore9_0 to TargetFramework.cs and ProjectBuilder.Validation.cs
  • Create BlazorPropertyInjectionShouldUseConstructorInjectionAnalyzer.cs
  • Create BlazorPropertyInjectionShouldUseConstructorInjectionFixer.cs
  • Create BlazorPropertyInjectionFixAllProvider.cs
  • Create tests (10 tests, all passing)
  • Create docs in docs/Rules/MA0187.md
  • Run documentation generator - all docs up to date
  • All tests pass
  • Address review feedback (IsCSharp12OrAbove positioning, numeric version, extension method, disabled by default)
  • Fix CI failures: wrap fixer/test files with #if CSHARP12_OR_GREATER (TypeDeclarationSyntax.ParameterList requires Roslyn 4.8+)
  • Fix CI roslyn4.8: replace [.. documentDiagnostics] collection expression with ImmutableArray.CreateRange(documentDiagnostics) to avoid CS9210
Original prompt

This section details on the original issue you should resolve

<issue_title>Blazor (.NET9+): migrate property injection to constructor injection</issue_title>
<issue_description>Is your feature request related to a problem? Please describe.
Since .NET9, it is possible to have constructor injection in Blazor components, instead of property injection: https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-9.0?view=aspnetcore-10.0#constructor-injection / https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-9.0#request-a-service-in-a-component

It would be nice to have an analyzer and a fix that helps detect and migrate property injection to constructor injection in Blazor components.

Describe the solution you'd like

Considering this code:

using Microsoft.AspNetCore.Components;

public partial class Component
{
    [Inject]
    protected NavigationManager Navigation { get; set; } = default!;

    private void HandleClick()
    {
        NavigationManager.NavigateTo("/counter");
    }
}

the injected dependency should be marked as a suggestion of fix, with the fix that would change the property to a constructor parameter like this:

using Microsoft.AspNetCore.Components;

public partial class Component(NavigationManager navigation)
{
    private void HandleClick()
    {
        navigation.NavigateTo("/counter");
    }
}

Describe alternatives you've considered
N/A

Additional context
I'm not sure if anyone would prefer the other way around? Detecting constructor injection to migrate to property injection? Constructor injection seems like an improvement (avoid defaut! and in somes cases, avoid more complicated init in OnInitializedAsync ).

The presence on an existing constructor in the class could complicate the fix, especially if async is involved :

partial class Home
{
    public Home(AdminClientService downstreamApi)
    {
        _downstreamApi = downstreamApi;
    }

    int _numberOfDevices;
    string _version = "v.?";
    readonly AdminClientService _downstreamApi;

    [Inject] IWebHostEnvironment _env { get; set; } = default!;

    protected override async Task OnInitializedAsync()
    {
        ICollection<DeviceListDto> devices = await _downstreamApi.GetDeviceListAsync()
           .ConfigureAwait(true);
        _numberOfDevices = devices.Count;

        _version = await CloudVersionService.GetCloudVersionAsync(_env.WebRootPath)
            .ConfigureAwait(false) ?? "";
    }
}

would need to migrate to:

partial class Home
{
    public Home(AdminClientService downstreamApi,
                         IWebHostEnvironment env)
    {
        _downstreamApi = downstreamApi;
        _env = env;
    }

    int _numberOfDevices;
    string _version = "v.?";
    readonly AdminClientService _downstreamApi;
    readonly IWebHostEnvironment _env;

    protected override async Task OnInitializedAsync()
    {
        ICollection<DeviceListDto> devices = await _downstreamApi.GetDeviceListAsync()
           .ConfigureAwait(true);
        _numberOfDevices = devices.Count;

        _version = await CloudVersionService.GetCloudVersionAsync(_env.WebRootPath)
            .ConfigureAwait(false) ?? "";
    }
}

Then IDE0290 could complete the job to migrate to primary constructor to get close to the simpler example from the doc.</issue_description>

<agent_instructions>Can you implement the logic to detect [Inject] properties in Razor component (class implementing Microsoft.AspNetCore.Components.IComponent). Report them as suggestions. Only report if ASP.NET Core 9 or greater (check assembly version).
Also, only report if primary constructor is supported by the current version of C#.

The fixer must create the primary constructor or update it if any and update all references to the property. Use Renamer.RenameSymbolAsync before removing the property, so it replaces all usages.
Be sure to add tests for the batch fixer with multiple injected properties.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@meziantou Do you think it should report @Inject directive? Maybe only when there is a partial class? @meziantou > AFAIK constructor injection requires it

I think you can do whatever you want in the @code {} section.

I'm not familiar enough with analyzers to tell if it is hard to tell the difference between @Inject (no code behind, maybe) and [Inject] (only used in code behind) ?

I think it's mostly checking if the file is generated or user-defined. It's trivial to exclude generated files in analyzers.

Maybe we can start with user-defined code and skip the code generated by the source generator.</comment_new>


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

…ibute in Blazor components

Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com>
Copilot AI changed the title [WIP] Migrate property injection to constructor injection in Blazor MA0187: Migrate Blazor [Inject] property injection to constructor injection Feb 27, 2026

public static bool IsCSharp12OrAbove(this LanguageVersion languageVersion)
{
return languageVersion >= LanguageVersion.CSharp12;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Also, move it between IsCSharp11OrAbove and IsCSharp13OrAbove

Suggested change
return languageVersion >= LanguageVersion.CSharp12;
return languageVersion >= (LanguageVersion)1200;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9779b79. Moved IsCSharp12OrAbove between IsCSharp11OrAbove and IsCSharp13OrAbove, and updated to use (LanguageVersion)1200 for consistency.


public void AnalyzeProperty(SymbolAnalysisContext context)
{
if (context.Compilation.GetCSharpLanguageVersion() < LanguageVersion.CSharp12)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Use the extension method

Suggested change
if (context.Compilation.GetCSharpLanguageVersion() < LanguageVersion.CSharp12)
if (!context.Compilation.GetCSharpLanguageVersion().IsCSharp12OrAbove())

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9779b79.

messageFormat: "Use constructor injection instead of [Inject] attribute",
RuleCategories.Design,
DiagnosticSeverity.Info,
isEnabledByDefault: true,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot The analyzer is still experimental, so disabled by default

Suggested change
isEnabledByDefault: true,
isEnabledByDefault: false,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9779b79.

… disable by default

Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com>
@meziantou meziantou marked this pull request as ready for review February 27, 2026 04:20
@meziantou
Copy link
Copy Markdown
Owner

@copilot CI is failing

…ion compatibility

Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 27, 2026

@copilot CI is failing

Fixed in 4143148. The CI was failing because TypeDeclarationSyntax.ParameterList and WithParameterList() (needed for primary constructor syntax) require Roslyn 4.8+. Wrapped the two fixer files (BlazorPropertyInjectionShouldUseConstructorInjectionFixer.cs, BlazorPropertyInjectionFixAllProvider.cs) and the test file with #if CSHARP12_OR_GREATER, following the same pattern used by PrimaryConstructorParameterShouldBeReadOnlyAnalyzer.cs.

@meziantou
Copy link
Copy Markdown
Owner

@copilot CI is failing

…reateRange

Co-authored-by: meziantou <509220+meziantou@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 27, 2026

@copilot CI is failing

Fixed in b510766. The roslyn4.8 build was failing with CS9210 because [.. documentDiagnostics] (a collection expression spreading into ImmutableArray<T>) isn't supported in that Roslyn version's runtime. Replaced it with ImmutableArray.CreateRange(documentDiagnostics).

This was referenced May 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Blazor (.NET9+): migrate property injection to constructor injection

2 participants