Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 28, 2025

Fix CA1869 false positive in top-level statements

  • Understand the analyzer implementation and test structure
  • Add detection for top-level statements in the analyzer
  • Add test cases for top-level statements (should not warn)
  • Run existing tests to ensure no regressions (78 tests pass)
  • Run dotnet format to ensure proper formatting
  • Code review completed with no issues
  • CodeQL security scan completed with no issues
  • Address PR feedback from @stephentoub

Summary

This PR fixes CA1869 (AvoidSingleUseOfLocalJsonSerializerOptions) to not report false positives in top-level statements. The analyzer was incorrectly warning about single-use JsonSerializerOptions in top-level programs, where the code only executes once at startup and caching is less impactful.

Changes Made

  1. Analyzer Fix: Added detection in AvoidSingleUseOfLocalJsonSerializerOptions.cs to check if the containing method is a top-level statements entry point using IsTopLevelStatementsEntryPointMethod()

  2. Test Coverage: Added three test cases covering different top-level statement scenarios:

    • Direct use of new JsonSerializerOptions as argument
    • Use of local variable with object initializer
    • Use of local variable with assignment
  3. Code Quality: Formatted code according to repository standards

All 78 tests pass (75 existing + 3 new).

Original prompt

This section details on the original issue you should resolve

<issue_title>CA1869: "Cache and reuse 'JsonSerializerOptions' instances" - false positives in top-level programs</issue_title>
<issue_description>### Description

CA1869 is caused by a local instance of JsonSerializerOptions being created and used once as the options argument of a Serialize or Deserialize call as this can cause performance degradation if the code is executed multiple times.

I initially found this bug in a top-level program where there was only a single Deserialize call in the whole file, so I thought it was a little strange, but upon further investigation found that this bug extends to other situations.

Reproduction Steps

There are 3 versions, all very similar, but each with potentially different solutions.

Version 1 - Top-level program

JsonSerializerOptions options = new()
{
    PropertyNameCaseInsensitive = true,
    ReadCommentHandling = JsonCommentHandling.Skip
};

var output = JsonSerializer.Deserialize<int[]>("[1,2,3]", options);

Image

Version 2 - In a class constructor:

    public Test()
    {
        JsonSerializerOptions options = new()
        {
            PropertyNameCaseInsensitive = true,
            ReadCommentHandling = JsonCommentHandling.Skip
        };

        var output = JsonSerializer.Deserialize<int[]>("[1,2,3]", options);
    }

Version 3 - In a loop inside a standard method:

    public void M()
    {
        for (int i = 0; i < 10; i++)
        {
            JsonSerializerOptions options = new()
            {
                PropertyNameCaseInsensitive = true,
                ReadCommentHandling = JsonCommentHandling.Skip
            };

            var output = JsonSerializer.Deserialize<int[]>("[1,2,3]", options);
        }
    }

Expected behavior

For version 1, I'm honestly not sure what should happen - maybe nothing, as the fix appears to make the code worse, not better.
For version 2, I could see the fixer creating a static variable to hold the options for use in each construction.
For version 3, I would hope the fixer would, at the very least, move the instance creation outside the loop, as duplicating it inside the loop doesn't improve the performance at all.

Actual behavior

After using the provided fixer, the following was produced:

Version 1 - Top-level program

JsonSerializerOptions jsonSerializerOptions = new()
{
    PropertyNameCaseInsensitive = true,
    ReadCommentHandling = JsonCommentHandling.Skip
};
JsonSerializerOptions options = jsonSerializerOptions;

var output = JsonSerializer.Deserialize<int[]>("[1,2,3]", options);

Version 2 - In a class constructor:

    public Test()
    {
        JsonSerializerOptions jsonSerializerOptions = new()
        {
            PropertyNameCaseInsensitive = true,
            ReadCommentHandling = JsonCommentHandling.Skip
        };
        JsonSerializerOptions options = jsonSerializerOptions;

        var output = JsonSerializer.Deserialize<int[]>("[1,2,3]", options);

Version 3 - In a loop inside a standard method:

    public void M()
    {
        for (int i = 0; i < 10; i++)
        {
            JsonSerializerOptions jsonSerializerOptions = new()
            {
                PropertyNameCaseInsensitive = true,
                ReadCommentHandling = JsonCommentHandling.Skip
            };
            JsonSerializerOptions options = jsonSerializerOptions;

            var output = JsonSerializer.Deserialize<int[]>("[1,2,3]", options);
        }
    }

Version 3 does also have an extra option, although after the one above, which alters the method to allow a JsonSerializerOptions instance to be passed in as a parameter, which is much better.

Regression?

The problem seems to also be in NET8, but not in NET7.

Known Workarounds

Ignore or suppress the rule.

Configuration

Using VS 17.13.0 Preview 4 in a NET9 console application, running on Windows 11 x64.

Other information

No response</issue_description>

<agent_instructions>Please check whether there are any unit tests for CA1869 in top-level statements. We do not want this analyzer to run in top-level statements. If it's issuing warnings, please fix it to not. Either way, ensure there's a test in place for this.</agent_instructions>

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

@eiriktsarpalis > After using the provided fixer

I'm pretty sure the analyzer in question doesn't come with a fixer. Can you clarify which suggestion you applied that resulted in these changes? The recommendations I'm seeing are unrelated to the analyzer per se:

Image</comment_new>
<comment_new>@eiriktsarpalis
It's unrelated. We intentionally didn'...


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

Copilot AI and others added 4 commits October 28, 2025 12:54
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix false positives for CA1869 in top-level programs Fix CA1869 false positive in top-level statements Oct 28, 2025
Copilot finished work on behalf of stephentoub October 28, 2025 13:12
Copilot AI requested a review from stephentoub October 28, 2025 13:12
@stephentoub stephentoub marked this pull request as ready for review October 28, 2025 13:44
@stephentoub stephentoub requested a review from a team as a code owner October 28, 2025 13:44
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes a false positive in CA1869 where the analyzer incorrectly warns about caching JsonSerializerOptions instances in top-level statements, which execute only once at program startup and therefore gain no benefit from caching.

Key changes:

  • Modified the analyzer to skip diagnostics when code is within a top-level statements entry point method
  • Added comprehensive test coverage for top-level statements scenarios
  • Updated imports ordering in the analyzer file

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
AvoidSingleUseOfLocalJsonSerializerOptions.cs Added logic to detect and skip top-level statements entry point methods; reorganized using directives
AvoidSingleUseOfLocalJsonSerializerOptionsTests.cs Added three test cases covering different patterns of JsonSerializerOptions usage in top-level statements
RulesMissingDocumentation.md Removed documentation entries for unrelated analyzer rules
.git-blame-ignore-revs Added commit SHA for the formatting change to exclude from git blame

Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
Copilot AI requested a review from stephentoub October 28, 2025 13:55
Copilot finished work on behalf of stephentoub October 28, 2025 13:55
@stephentoub stephentoub enabled auto-merge (squash) October 28, 2025 15:58
@stephentoub
Copy link
Member

/ba-g dead letter

Copy link
Member

@eiriktsarpalis eiriktsarpalis left a comment

Choose a reason for hiding this comment

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

Thanks. I realize we're a bit late, but any chance we could try backporting this to 10 e.g. via servicing?

@stephentoub stephentoub merged commit 306c1a5 into main Oct 29, 2025
27 of 29 checks passed
@stephentoub stephentoub deleted the copilot/fix-ca1869-jsonserializeroptions branch October 29, 2025 10:04
@stephentoub
Copy link
Member

Thanks. I realize we're a bit late, but any chance we could try backporting this to 10 e.g. via servicing?

You can if you'd like.

@eiriktsarpalis
Copy link
Member

Thanks. I realize we're a bit late, but any chance we could try backporting this to 10 e.g. via servicing?

You can if you'd like.

Paging @jeffhandley and @ericstj for a second opinion. This does not meet the bar for servicing by any measure, but it's a known quality of life issue that shows itself when using top-level statements in demo apps.

@baronfel
Copy link
Member

This analyzer is shipped in the SDK, so you have more ship vehicles available. It could ship in 10.0.200 for example without any process churn, and backports for 10.0.200 are wide open with no bar aside from normal development checks, breaking change notifications, and so on.

@eiriktsarpalis
Copy link
Member

Awesome! What branch should I be targeting for a backport?

@baronfel
Copy link
Member

release/10.0.2xx is the branch for that.

In the SDK there are almost always two 'live' branches:

  • main is the next major version (this is being prepped for 11 right now)
  • release/X.Y.Zxx for the next SDK feature band release.

Codeflow is set up so that if you merge into the next feature band release your work will flow to main, so most folks working on the SDK will target the feature band release by default, unless the change is so impactful to only be safe for the next major, or if it requires other next-major-only features to light up.

@eiriktsarpalis
Copy link
Member

/backport to release/10.0.2xx

@github-actions
Copy link
Contributor

Started backporting to release/10.0.2xx (link to workflow run)

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.

CA1869: "Cache and reuse 'JsonSerializerOptions' instances" - false positives in top-level programs

4 participants