Skip to content

perf: Use String Interpolation In Code Generation - #2278

Merged
glennawatson merged 4 commits into
mainfrom
perf_UseStringInterpolationInCodeHeneration
Jul 26, 2026
Merged

perf: Use String Interpolation In Code Generation#2278
glennawatson merged 4 commits into
mainfrom
perf_UseStringInterpolationInCodeHeneration

Conversation

@jgarciadelanoceda

@jgarciadelanoceda jgarciadelanoceda commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

What kind of change does this PR introduce?

Performace, it uses InterpolatedStrings instead of concatenating strings with a + on the code Generation. Initial change on #2277

What is the new behavior?

refitQueryBuilder.AddPreEscapedKey($"{refitQueryValue_key}.Id", refitUseDefaultFormatting ? (global::Refit.GeneratedRequestRunner.FormatInvariant(refitQueryValue_items_Id, null)) : global::Refit.GeneratedRequestRunner.FormatUrlParameter(refitSettings, refitQueryValue_items_Id, ______itemsAttributeProvider, typeof(global::System.Collections.Generic.IReadOnlyList<global::RefitGeneratorTest.Item>)), false);

What is the current behavior?

refitQueryBuilder.AddPreEscapedKey(refitQueryValue_key + "." + "Id", refitUseDefaultFormatting ? (global::Refit.GeneratedRequestRunner.FormatInvariant(refitQueryValue_items_Id, null)) : global::Refit.GeneratedRequestRunner.FormatUrlParameter(refitSettings, refitQueryValue_items_Id, ______itemsAttributeProvider, typeof(global::System.Collections.Generic.IReadOnlyList<global::RefitGeneratorTest.Item>)), false);

What might this PR break?

Different scape sequences for interpolatedStrings? If it's so the code generated could not compile

Checklist

  • I have read the Contribute guide
  • Tests have been added or updated (for bug fixes / features)
  • Docs have been added or updated (for bug fixes / features)
  • Changes target the main branch
  • PR title follows Conventional Commits

Additional information

I change the name of StringInterpolationHandler to StringInterpolationBuilder to be more accurate on the naming

@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.98%. Comparing base (2f16caa) to head (c7c0b9a).

Additional details and impacted files
@@             Coverage Diff             @@
##              main    #2278      +/-   ##
===========================================
- Coverage   100.00%   99.98%   -0.02%     
===========================================
  Files          191      191              
  Lines         9902     9927      +25     
  Branches      1904     1908       +4     
===========================================
+ Hits          9902     9926      +24     
- Partials         0        1       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@glennawatson

Copy link
Copy Markdown
Contributor

Looks good, some tests that I would like to add and fix any issues, generated by AI so adjust as needed:

using Refit.Generator;

namespace Refit.GeneratorTests;

/// <summary>Tests interpolated strings emitted by the generator.</summary>
public sealed class InterpolatedStringGenerationTests
{
    /// <summary>Verifies literal interpolation braces are doubled.</summary>
    /// <returns>A task representing the test.</returns>
    [Test]
    public async Task BuilderEscapesLiteralBraces()
    {
        var actual = new Emitter.InterpolatedStringBuilder()
            .AppendLiteral("{zip}")
            .AppendExpression("value")
            .Build();

        await Assert.That(actual).IsEqualTo("$\"{{zip}}{value}\"");
    }

    /// <summary>Verifies braces in a nested alias produce valid generated code.</summary>
    /// <returns>A task representing the test.</returns>
    [Test]
    public Task NestedAliasContainingBracesCompiles() =>
        AssertGeneratedCodeCompiles(
            """
            using System.Threading.Tasks;
            using Refit;

            namespace RefitGeneratorTest;

            public sealed class Inner
            {
                [AliasAs("{zip}")]
                public string Zip { get; set; } = "";
            }

            public sealed class QueryModel
            {
                public Inner Nested { get; set; } = new();
            }

            public interface IGeneratedClient
            {
                [Get("/query")]
                Task<string> Find([Query] QueryModel query);
            }
            """);

    /// <summary>Verifies braces in a serializer name produce valid generated code.</summary>
    /// <returns>A task representing the test.</returns>
    [Test]
    public Task NestedSerializerNameContainingBracesCompiles() =>
        AssertGeneratedCodeCompiles(
            """
            using System.Text.Json.Serialization;
            using System.Threading.Tasks;
            using Refit;

            namespace RefitGeneratorTest;

            public sealed class Inner
            {
                [JsonPropertyName("{zip}")]
                public string Zip { get; set; } = "";
            }

            public sealed class QueryModel
            {
                public Inner Nested { get; set; } = new();
            }

            public interface IGeneratedClient
            {
                [Get("/query")]
                Task<string> Find([Query] QueryModel query);
            }
            """);

    /// <summary>Verifies braces in query prefixes and delimiters produce valid generated code.</summary>
    /// <returns>A task representing the test.</returns>
    [Test]
    public Task QueryPrefixAndDelimiterContainingBracesCompile() =>
        AssertGeneratedCodeCompiles(
            """
            using System.Threading.Tasks;
            using Refit;

            namespace RefitGeneratorTest;

            public sealed class Inner
            {
                public string Value { get; set; } = "";
            }

            public sealed class QueryModel
            {
                [Query("{delimiter}", "{prefix}")]
                public Inner Nested { get; set; } = new();
            }

            public interface IGeneratedClient
            {
                [Get("/query")]
                Task<string> Find([Query] QueryModel query);
            }
            """);

    /// <summary>Verifies braces in authorization schemes produce valid generated code.</summary>
    /// <returns>A task representing the test.</returns>
    [Test]
    public Task AuthorizationSchemeContainingBracesCompiles() =>
        AssertGeneratedCodeCompiles(
            """
            using System.Threading.Tasks;
            using Refit;

            namespace RefitGeneratorTest;

            public interface IGeneratedClient
            {
                [Get("/query")]
                Task<string> Find([Authorize("{Bearer}")] string token);
            }
            """);

    /// <summary>Runs the generator and verifies its output compiles.</summary>
    /// <param name="source">The source being compiled.</param>
    /// <returns>A task representing the assertion.</returns>
    private static async Task AssertGeneratedCodeCompiles(string source)
    {
        var result = Fixture.RunGenerator(source, generatedRequestBuilding: true);

        await Assert.That(result.CompilationErrors).IsEmpty();
    }
}

@jgarciadelanoceda

Copy link
Copy Markdown
Collaborator Author
/// <summary>Verifies literal interpolation braces are doubled.</summary>
    /// <returns>A task representing the test.</returns>
    [Test]
    public async Task BuilderEscapesLiteralBraces()
    {
        var actual = new Emitter.InterpolatedStringBuilder()
            .AppendLiteral("{zip}")
            .AppendExpression("value")
            .Build();

        await Assert.That(actual).IsEqualTo("$\"{{zip}}{value}\"");
    }

    /// <summary>Verifies braces in a nested alias produce valid generated code.</summary>
    /// <returns>A task representing the test.</returns>
    [Test]
    public Task NestedAliasContainingBracesCompiles() =>
        AssertGeneratedCodeCompiles(
            """
            using System.Threading.Tasks;
            using Refit;

            namespace RefitGeneratorTest;

            public sealed class Inner
            {
                [AliasAs("{zip}")]
                public string Zip { get; set; } = "";
            }

            public sealed class QueryModel
            {
                public Inner Nested { get; set; } = new();
            }

            public interface IGeneratedClient
            {
                [Get("/query")]
                Task<string> Find([Query] QueryModel query);
            }
            """);

    /// <summary>Verifies braces in a serializer name produce valid generated code.</summary>
    /// <returns>A task representing the test.</returns>
    [Test]
    public Task NestedSerializerNameContainingBracesCompiles() =>
        AssertGeneratedCodeCompiles(
            """
            using System.Text.Json.Serialization;
            using System.Threading.Tasks;
            using Refit;

            namespace RefitGeneratorTest;

            public sealed class Inner
            {
                [JsonPropertyName("{zip}")]
                public string Zip { get; set; } = "";
            }

            public sealed class QueryModel
            {
                public Inner Nested { get; set; } = new();
            }

            public interface IGeneratedClient
            {
                [Get("/query")]
                Task<string> Find([Query] QueryModel query);
            }
            """);

    /// <summary>Verifies braces in query prefixes and delimiters produce valid generated code.</summary>
    /// <returns>A task representing the test.</returns>
    [Test]
    public Task QueryPrefixAndDelimiterContainingBracesCompile() =>
        AssertGeneratedCodeCompiles(
            """
            using System.Threading.Tasks;
            using Refit;

            namespace RefitGeneratorTest;

            public sealed class Inner
            {
                public string Value { get; set; } = "";
            }

            public sealed class QueryModel
            {
                [Query("{delimiter}", "{prefix}")]
                public Inner Nested { get; set; } = new();
            }

            public interface IGeneratedClient
            {
                [Get("/query")]
                Task<string> Find([Query] QueryModel query);
            }
            """);

    /// <summary>Verifies braces in authorization schemes produce valid generated code.</summary>
    /// <returns>A task representing the test.</returns>
    [Test]
    public Task AuthorizationSchemeContainingBracesCompiles() =>
        AssertGeneratedCodeCompiles(
            """
            using System.Threading.Tasks;
            using Refit;

            namespace RefitGeneratorTest;

            public interface IGeneratedClient
            {
                [Get("/query")]
                Task<string> Find([Authorize("{Bearer}")] string token);
            }
            """);

I liked it what it created, however I tried to move some of the test to QueryRequestBuildingLiveTests.. To verify that the paths (Reflection vs code generation were working fine), but I couldn't fix the Reflection on NestedSerializerNameContainingBracesCompiles and I do not think that should be neeeded

@sonarqubecloud

Copy link
Copy Markdown

@glennawatson
glennawatson merged commit 6501f88 into main Jul 26, 2026
16 of 17 checks passed
@glennawatson
glennawatson deleted the perf_UseStringInterpolationInCodeHeneration branch July 26, 2026 00:30
@glennawatson

Copy link
Copy Markdown
Contributor

Thanks again. Just added another edge case to the tests. I'll do a release in a week or so on this one.

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.

2 participants