From 0d804e99da97f140a40f788e3db86095ba827ed4 Mon Sep 17 00:00:00 2001
From: Glenn Watson <5834289+glennawatson@users.noreply.github.com>
Date: Sat, 20 Jun 2026 03:47:05 +1000
Subject: [PATCH 01/13] feat: add generated request builder pipeline
---
.../InterfaceStubGenerator.Roslyn48.csproj | 7 +-
.../InterfaceStubGenerator.Roslyn50.csproj | 10 +-
src/InterfaceStubGenerator.Shared/Emitter.cs | 871 +++++++++++++++---
.../ITypeSymbolExtensions.cs | 41 +-
.../ImmutableEquatableArray.cs | 111 ++-
.../ImmutableEquatableArrayExtensions.cs | 14 +-
.../ImmutableEquatableArrayFactory.cs | 42 +
.../ImmutableEquatableArrayOfT.cs | 120 ---
.../IncrementalValuesProviderExtensions.cs | 12 +-
.../InterfaceStubGenerator.Shared.projitems | 38 -
.../InterfaceStubGenerator.Shared.shproj | 13 -
.../InterfaceStubGenerator.cs | 84 --
.../InterfaceStubGeneratorV2.cs | 157 ++++
.../IsExternalInit.cs | 3 +-
.../Models/BodyBufferMode.cs | 20 +
.../Models/ContextGenerationModel.cs | 2 +
.../Models/HeaderModel.cs | 11 +
.../Models/InterfaceGenerationContext.cs | 2 +
.../Models/InterfaceModel.cs | 4 +
.../Models/InterfacePropertyModel.cs | 25 +
.../Models/MethodModel.cs | 2 +
.../Models/RequestModel.cs | 38 +
.../Models/RequestParameterKind.cs | 26 +
.../Models/RequestParameterModel.cs | 23 +
.../Models/WellKnownTypes.cs | 23 +-
.../Parser.Request.cs | 730 +++++++++++++++
src/InterfaceStubGenerator.Shared/Parser.cs | 372 ++++++--
.../Polyfills/{IndexRange.cs => Index.cs} | 9 +-
.../SourceWriter.cs | 31 +-
.../UniqueNameBuilder.cs | 38 +-
src/Polyfills/HashCode.cs | 181 ++++
src/Polyfills/Index.cs | 84 ++
src/Polyfills/Range.cs | 81 ++
src/Refit.slnx | 1 -
...PooledBufferWriter.Stream.NETStandard21.cs | 2 +-
.../Buffers/PooledBufferWriter.Stream.cs | 4 +-
.../CachedRequestBuilderImplementation.cs | 7 +-
src/Refit/CamelCaseStringEnumConverter.cs | 2 +-
src/Refit/CloseGenericMethodKey.cs | 15 +-
src/Refit/GeneratedRequestRunner.cs | 758 +++++++++++++++
src/Refit/IRequestBuilder.cs | 3 +
src/Refit/MethodTableKey.cs | 24 +-
src/Refit/PropertyAttribute.cs | 2 +-
.../PublicAPI/net10.0/PublicAPI.Shipped.txt | 9 +
.../PublicAPI/net11.0/PublicAPI.Shipped.txt | 9 +
.../PublicAPI/net462/PublicAPI.Shipped.txt | 9 +
.../PublicAPI/net470/PublicAPI.Shipped.txt | 9 +
.../PublicAPI/net471/PublicAPI.Shipped.txt | 9 +
.../PublicAPI/net472/PublicAPI.Shipped.txt | 9 +
.../PublicAPI/net48/PublicAPI.Shipped.txt | 9 +
.../PublicAPI/net481/PublicAPI.Shipped.txt | 9 +
.../PublicAPI/net8.0/PublicAPI.Shipped.txt | 9 +
.../PublicAPI/net9.0/PublicAPI.Shipped.txt | 9 +
src/Refit/PushStreamContent.cs | 4 +-
src/Refit/RequestBuilder.cs | 6 +-
src/Refit/RequestBuilderFactory.cs | 12 +-
.../RequestBuilderImplementation.Execution.cs | 6 +-
...stBuilderImplementation.RequestBuilding.cs | 7 +-
src/Refit/RequestBuilderImplementation.cs | 5 +-
src/Refit/RestMethodInfoInternal.cs | 4 +-
src/Refit/ValueStringBuilder.cs | 20 +-
src/Refit/targets/refit.props | 1 +
src/Refit/targets/refit.targets | 1 +
src/Shared/UniqueName.cs | 8 +-
.../AotSafeAssertionExtensions.cs | 26 +
.../CollectibleAssemblyLoadContext.cs | 42 +
src/tests/Refit.GeneratorTests/Fixture.cs | 223 ++++-
.../GeneratedRequestBuildingTests.cs | 252 +++++
.../GeneratorComponentTests.cs | 88 +-
.../GeneratorTestResult.cs | 74 ++
.../InterfaceStubGeneratorTests.cs | 20 +-
.../LiveCompilationTests.cs | 134 +++
...ShouldEmitAllFiles#Generated.g.verified.cs | 6 +-
...mitAllFiles#IGeneratedClient.g.verified.cs | 2 +
...itAllFiles#PreserveAttribute.g.verified.cs | 2 +
...nterfacesSmokeTest#Generated.g.verified.cs | 14 +-
...terfacesSmokeTest#IGitHubApi.g.verified.cs | 2 +
...okeTest#IGitHubApiDisposable.g.verified.cs | 2 +
...esSmokeTest#INestedGitHubApi.g.verified.cs | 2 +
...sSmokeTest#PreserveAttribute.g.verified.cs | 2 +
...NamespaceSmokeTest#Generated.g.verified.cs | 6 +-
...est#IServiceWithoutNamespace.g.verified.cs | 2 +
...eSmokeTest#PreserveAttribute.g.verified.cs | 2 +
...faceTest#IContainedInterface.g.verified.cs | 2 +
...ceMethod#IGeneratedInterface.g.verified.cs | 2 +
...terfaceMethod#IBaseInterface.g.verified.cs | 2 +
...ceMethod#IGeneratedInterface.g.verified.cs | 2 +
...ableTest#IGeneratedInterface.g.verified.cs | 2 +
...paceTest#IGeneratedInterface.g.verified.cs | 2 +
...RefitBaseTest#IBaseInterface.g.verified.cs | 2 +
...itBaseTest#IDerivedInterface.g.verified.cs | 2 +
...nstraint#IGeneratedInterface.g.verified.cs | 2 +
...acesWithDifferentCasing#IApi.g.verified.cs | 2 +
...cesWithDifferentCasing#Iapi1.g.verified.cs | 2 +
...sWithDifferentSignature#IApi.g.verified.cs | 2 +
...WithDifferentSignature#IApi1.g.verified.cs | 2 +
...paceTest#IGeneratedInterface.g.verified.cs | 2 +
...eDiagnostic#IGeneratedClient.g.verified.cs | 2 +
...BaseTest#IGeneratedInterface.g.verified.cs | 2 +
...RefitBaseTest#IBaseInterface.g.verified.cs | 2 +
...BaseTest#IGeneratedInterface.g.verified.cs | 2 +
...Constraints#IGeneratedClient.g.verified.cs | 2 +
...teParameter#IGeneratedClient.g.verified.cs | 2 +
...teParameter#IGeneratedClient.g.verified.cs | 2 +
...teParameter#IGeneratedClient.g.verified.cs | 2 +
...teParameter#IGeneratedClient.g.verified.cs | 2 +
...tReturnTask#IGeneratedClient.g.verified.cs | 2 +
...tReturnTask#IGeneratedClient.g.verified.cs | 2 +
...kShouldWork#IGeneratedClient.g.verified.cs | 2 +
...tReturnTask#IGeneratedClient.g.verified.cs | 2 +
...kShouldWork#IGeneratedClient.g.verified.cs | 2 +
...IObservable#IGeneratedClient.g.verified.cs | 2 +
...lableObject#IGeneratedClient.g.verified.cs | 2 +
...leValueType#IGeneratedClient.g.verified.cs | 2 +
...pportedType#IGeneratedClient.g.verified.cs | 2 +
...eShouldWork#IGeneratedClient.g.verified.cs | 2 +
...kShouldWork#IGeneratedClient.g.verified.cs | 2 +
.../AuthenticatedClientHandlerTests.cs | 4 +-
.../Refit.Tests/CachedRequestBuilderTests.cs | 5 +-
...rSeparatedPropertyNamesContractResolver.cs | 5 +-
src/tests/Refit.Tests/Foo.cs | 5 +-
src/tests/Refit.Tests/IHaveDims.cs | 15 +-
src/tests/Refit.Tests/ModuleInitializer.cs | 23 -
src/tests/Refit.Tests/MultipartTests.cs | 8 +-
.../Refit.Tests/NamespaceCollisionApi.cs | 5 +-
src/tests/Refit.Tests/NamespaceOverlapApi.cs | 5 +-
src/tests/Refit.Tests/Refit.Tests.csproj | 20 +-
.../RequestBuilderTests.Dictionaries.cs | 3 +
.../Refit.Tests/RestServiceExceptions.cs | 5 +-
.../RestServiceIntegrationTests.cs | 5 +-
.../Refit.Tests/SerializedContentTests.cs | 40 +-
.../Refit.Tests/TestUrlParameterFormatter.cs | 10 +-
src/tests/Refit.Tests/TypeCollisionApiA.cs | 5 +-
src/tests/Refit.Tests/TypeCollisionApiB.cs | 5 +-
...arpIncrementalSourceGeneratorVerifier`1.cs | 51 -
.../CSharpSourceGeneratorVerifier`1.cs | 34 -
.../Verifiers/CSharpVerifierHelper.cs | 35 -
.../Refit.Tests/XmlContentSerializerTests.cs | 6 +-
138 files changed, 4507 insertions(+), 963 deletions(-)
create mode 100644 src/InterfaceStubGenerator.Shared/ImmutableEquatableArrayFactory.cs
delete mode 100644 src/InterfaceStubGenerator.Shared/ImmutableEquatableArrayOfT.cs
delete mode 100644 src/InterfaceStubGenerator.Shared/InterfaceStubGenerator.Shared.projitems
delete mode 100644 src/InterfaceStubGenerator.Shared/InterfaceStubGenerator.Shared.shproj
delete mode 100644 src/InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs
create mode 100644 src/InterfaceStubGenerator.Shared/InterfaceStubGeneratorV2.cs
create mode 100644 src/InterfaceStubGenerator.Shared/Models/BodyBufferMode.cs
create mode 100644 src/InterfaceStubGenerator.Shared/Models/HeaderModel.cs
create mode 100644 src/InterfaceStubGenerator.Shared/Models/InterfacePropertyModel.cs
create mode 100644 src/InterfaceStubGenerator.Shared/Models/RequestModel.cs
create mode 100644 src/InterfaceStubGenerator.Shared/Models/RequestParameterKind.cs
create mode 100644 src/InterfaceStubGenerator.Shared/Models/RequestParameterModel.cs
create mode 100644 src/InterfaceStubGenerator.Shared/Parser.Request.cs
rename src/InterfaceStubGenerator.Shared/Polyfills/{IndexRange.cs => Index.cs} (89%)
create mode 100644 src/Polyfills/HashCode.cs
create mode 100644 src/Polyfills/Index.cs
create mode 100644 src/Polyfills/Range.cs
create mode 100644 src/Refit/GeneratedRequestRunner.cs
create mode 100644 src/tests/Refit.GeneratorTests/AotSafeAssertionExtensions.cs
create mode 100644 src/tests/Refit.GeneratorTests/CollectibleAssemblyLoadContext.cs
create mode 100644 src/tests/Refit.GeneratorTests/GeneratedRequestBuildingTests.cs
rename src/tests/{Refit.Tests => Refit.GeneratorTests}/GeneratorComponentTests.cs (77%)
create mode 100644 src/tests/Refit.GeneratorTests/GeneratorTestResult.cs
rename src/tests/{Refit.Tests => Refit.GeneratorTests}/InterfaceStubGeneratorTests.cs (84%)
create mode 100644 src/tests/Refit.GeneratorTests/LiveCompilationTests.cs
rename src/tests/{Refit.Tests => Refit.GeneratorTests}/_snapshots/InterfaceStubGeneratorTests.FindInterfacesSmokeTest#Generated.g.verified.cs (53%)
rename src/tests/{Refit.Tests => Refit.GeneratorTests}/_snapshots/InterfaceStubGeneratorTests.FindInterfacesSmokeTest#IGitHubApi.g.verified.cs (98%)
rename src/tests/{Refit.Tests => Refit.GeneratorTests}/_snapshots/InterfaceStubGeneratorTests.FindInterfacesSmokeTest#IGitHubApiDisposable.g.verified.cs (91%)
rename src/tests/{Refit.Tests => Refit.GeneratorTests}/_snapshots/InterfaceStubGeneratorTests.FindInterfacesSmokeTest#INestedGitHubApi.g.verified.cs (97%)
rename src/tests/{Refit.Tests => Refit.GeneratorTests}/_snapshots/InterfaceStubGeneratorTests.FindInterfacesSmokeTest#PreserveAttribute.g.verified.cs (86%)
rename src/tests/{Refit.Tests => Refit.GeneratorTests}/_snapshots/InterfaceStubGeneratorTests.GenerateInterfaceStubsWithoutNamespaceSmokeTest#Generated.g.verified.cs (70%)
rename src/tests/{Refit.Tests => Refit.GeneratorTests}/_snapshots/InterfaceStubGeneratorTests.GenerateInterfaceStubsWithoutNamespaceSmokeTest#IServiceWithoutNamespace.g.verified.cs (92%)
rename src/tests/{Refit.Tests => Refit.GeneratorTests}/_snapshots/InterfaceStubGeneratorTests.GenerateInterfaceStubsWithoutNamespaceSmokeTest#PreserveAttribute.g.verified.cs (86%)
delete mode 100644 src/tests/Refit.Tests/ModuleInitializer.cs
delete mode 100644 src/tests/Refit.Tests/Verifiers/CSharpIncrementalSourceGeneratorVerifier`1.cs
delete mode 100644 src/tests/Refit.Tests/Verifiers/CSharpSourceGeneratorVerifier`1.cs
delete mode 100644 src/tests/Refit.Tests/Verifiers/CSharpVerifierHelper.cs
diff --git a/src/InterfaceStubGenerator.Roslyn48/InterfaceStubGenerator.Roslyn48.csproj b/src/InterfaceStubGenerator.Roslyn48/InterfaceStubGenerator.Roslyn48.csproj
index 2aa9a65be..8f2f32893 100644
--- a/src/InterfaceStubGenerator.Roslyn48/InterfaceStubGenerator.Roslyn48.csproj
+++ b/src/InterfaceStubGenerator.Roslyn48/InterfaceStubGenerator.Roslyn48.csproj
@@ -20,6 +20,11 @@
+
+
+
+
+
@@ -29,6 +34,4 @@
-
-
diff --git a/src/InterfaceStubGenerator.Roslyn50/InterfaceStubGenerator.Roslyn50.csproj b/src/InterfaceStubGenerator.Roslyn50/InterfaceStubGenerator.Roslyn50.csproj
index a32501300..ea19677f0 100644
--- a/src/InterfaceStubGenerator.Roslyn50/InterfaceStubGenerator.Roslyn50.csproj
+++ b/src/InterfaceStubGenerator.Roslyn50/InterfaceStubGenerator.Roslyn50.csproj
@@ -18,6 +18,14 @@
+
+
+
+
+
+
+
+
$(BuildVersion)
@@ -25,6 +33,4 @@
-
-
diff --git a/src/InterfaceStubGenerator.Shared/Emitter.cs b/src/InterfaceStubGenerator.Shared/Emitter.cs
index 42221f710..cee170117 100644
--- a/src/InterfaceStubGenerator.Shared/Emitter.cs
+++ b/src/InterfaceStubGenerator.Shared/Emitter.cs
@@ -10,12 +10,36 @@ namespace Refit.Generator;
/// Emits the generated source code for Refit interface implementations.
internal static class Emitter
{
+ /// The generated literal for .
+ private const string FalseLiteral = "false";
+
+ /// The generated literal for .
+ private const string TrueLiteral = "true";
+
+ /// The C# global namespace alias prefix.
+ private const string GlobalPrefix = "global::";
+
/// The variable name used for the cached type parameter array field.
private const string TypeParameterVariableName = "______typeParameters";
/// Indentation levels spanned by the generated namespace and class nesting.
private const int NamespaceAndClassIndentation = 2;
+ /// Initial buffer size for shared generated infrastructure source.
+ private const int SharedGeneratedSourceBaseCapacity = 2048;
+
+ /// Estimated characters needed for one generated factory registration.
+ private const int EstimatedFactoryRegistrationCapacity = 256;
+
+ /// Initial buffer size for a typical generated interface implementation.
+ private const int InterfaceSourceBaseCapacity = 4096;
+
+ /// Estimated characters needed for one generated method body.
+ private const int EstimatedMethodSourceCapacity = 768;
+
+ /// Estimated characters needed for one generated property.
+ private const int EstimatedPropertySourceCapacity = 128;
+
/// Emits the shared preserve attribute and factory registration code.
/// The context generation model describing the interfaces.
/// Callback used to add generated source files.
@@ -38,6 +62,8 @@ public static void EmitSharedCode(
var attributeText = $$"""
+ // This file is generated into consumer projects; suppress all analyzers so
+ // consumer analyzer policy does not report Refit implementation details.
#pragma warning disable
namespace {{model.RefitInternalNamespace}}
{
@@ -61,49 +87,45 @@ sealed class PreserveAttribute : global::System.Attribute
// add the attribute text
addSource("PreserveAttribute.g.cs", SourceText.From(attributeText, Encoding.UTF8));
- var generatedFactoryRegistrations = string.Join(
- "\n",
- model.Interfaces
- .Where(static interfaceModel => !interfaceModel.ClassDeclaration.Contains("<"))
- .Select(static interfaceModel =>
- " global::Refit.RestService.RegisterGeneratedFactory(typeof("
- + $"{interfaceModel.InterfaceDisplayName}), static (client, requestBuilder) => new "
- + $"global::Refit.Implementation.Generated.{interfaceModel.Ns}{interfaceModel.ClassSuffix}"
- + "(client, requestBuilder));"));
-
const string dynamicDependencyLine =
"[System.Diagnostics.CodeAnalysis.DynamicDependency("
+ "System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All, "
+ "typeof(global::Refit.Implementation.Generated))]";
- var generatedClassText = $$"""
-
- #pragma warning disable
- namespace Refit.Implementation
- {
-
- ///
- [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
- [global::System.Diagnostics.DebuggerNonUserCode]
- [{{model.PreserveAttributeDisplayName}}]
- [global::System.Reflection.Obfuscation(Exclude=true)]
- [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
- internal static partial class Generated
- {
- #if NET5_0_OR_GREATER
- [System.Runtime.CompilerServices.ModuleInitializer]
- {{dynamicDependencyLine}}
- public static void Initialize()
- {
- {{generatedFactoryRegistrations}}
- }
- #endif
- }
- }
- #pragma warning restore
-
- """;
- addSource("Generated.g.cs", SourceText.From(generatedClassText, Encoding.UTF8));
+ var generatedSource = new SourceWriter(EstimateSharedSourceCapacity(model));
+ generatedSource.WriteLine(
+ $$"""
+
+ // This file is generated into consumer projects; suppress all analyzers so
+ // consumer analyzer policy does not report Refit implementation details.
+ #pragma warning disable
+ namespace Refit.Implementation
+ {
+
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ [global::System.Diagnostics.DebuggerNonUserCode]
+ [{{model.PreserveAttributeDisplayName}}]
+ [global::System.Reflection.Obfuscation(Exclude=true)]
+ [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)]
+ internal static partial class Generated
+ {
+ #if NET5_0_OR_GREATER
+ [System.Runtime.CompilerServices.ModuleInitializer]
+ {{dynamicDependencyLine}}
+ public static void Initialize()
+ {
+ """);
+ WriteGeneratedFactoryRegistrations(generatedSource, model.Interfaces);
+ generatedSource.WriteLine(
+ """
+ }
+ #endif
+ }
+ }
+ #pragma warning restore
+ """);
+ addSource("Generated.g.cs", generatedSource.ToSourceText());
}
/// Emits the generated implementation source for a single interface.
@@ -111,7 +133,7 @@ public static void Initialize()
/// The generated source text for the interface implementation.
public static SourceText EmitInterface(InterfaceModel model)
{
- var source = new SourceWriter();
+ var source = new SourceWriter(EstimateInterfaceSourceCapacity(model));
// if nullability is supported emit the nullable directive
if (model.Nullability != Nullability.None)
@@ -122,6 +144,8 @@ public static SourceText EmitInterface(InterfaceModel model)
source.WriteLine(
$$"""
+ // This file is generated into consumer projects; suppress all analyzers so
+ // consumer analyzer policy does not report Refit implementation details.
#pragma warning disable
namespace Refit.Implementation
{
@@ -163,15 +187,20 @@ partial class {{model.Ns}}{{model.ClassDeclaration}}
var uniqueNames = new UniqueNameBuilder();
uniqueNames.Reserve(model.MemberNames);
+ foreach (var property in model.Properties)
+ {
+ WriteInterfaceProperty(source, property);
+ }
+
// Handle Refit Methods
foreach (var method in model.RefitMethods)
{
- WriteRefitMethod(source, method, true, uniqueNames);
+ WriteRefitMethod(source, method, true, model, uniqueNames);
}
foreach (var method in model.DerivedRefitMethods)
{
- WriteRefitMethod(source, method, false, uniqueNames);
+ WriteRefitMethod(source, method, false, model, uniqueNames);
}
// Handle non-refit Methods that aren't static or properties or have a method body
@@ -198,10 +227,56 @@ partial class {{model.Ns}}{{model.ClassDeclaration}}
return source.ToSourceText();
}
+ /// Estimates the initial buffer size for shared generated infrastructure source.
+ /// The context generation model describing the interfaces.
+ /// The estimated source buffer capacity.
+ private static int EstimateSharedSourceCapacity(ContextGenerationModel model) =>
+ SharedGeneratedSourceBaseCapacity + (model.Interfaces.Count * EstimatedFactoryRegistrationCapacity);
+
+ /// Estimates the initial buffer size for one generated interface implementation.
+ /// The interface model being emitted.
+ /// The estimated source buffer capacity.
+ private static int EstimateInterfaceSourceCapacity(InterfaceModel model)
+ {
+ var methodCount =
+ model.RefitMethods.Count + model.DerivedRefitMethods.Count + model.NonRefitMethods.Count;
+ return InterfaceSourceBaseCapacity
+ + (methodCount * EstimatedMethodSourceCapacity)
+ + (model.Properties.Count * EstimatedPropertySourceCapacity);
+ }
+
+ /// Writes the generated factory registrations for non-generic interfaces.
+ /// The source writer to emit to.
+ /// The parsed interface models.
+ private static void WriteGeneratedFactoryRegistrations(
+ SourceWriter source,
+ ImmutableEquatableArray interfaces)
+ {
+ for (var i = 0; i < interfaces.Count; i++)
+ {
+ var interfaceModel = interfaces[i];
+ if (interfaceModel.ClassDeclaration.Contains("<"))
+ {
+ continue;
+ }
+
+ source.WriteLine(" global::Refit.RestService.RegisterGeneratedFactory(");
+ source.Append(" typeof(");
+ source.Append(interfaceModel.InterfaceDisplayName);
+ source.WriteLine("),");
+ source.Append(
+ " static (client, requestBuilder) => new global::Refit.Implementation.Generated.");
+ source.Append(interfaceModel.Ns);
+ source.Append(interfaceModel.ClassSuffix);
+ source.WriteLine("(client, requestBuilder));");
+ }
+ }
+
/// Generates the body of the Refit method.
/// The source writer to emit to.
/// The method model being emitted.
/// True if directly from the type we're generating for, false for methods found on base interfaces.
+ /// The interface model being emitted.
/// Contains the unique member names in the interface scope.
[SuppressMessage(
"Usage",
@@ -212,9 +287,16 @@ private static void WriteRefitMethod(
SourceWriter source,
MethodModel methodModel,
bool isTopLevel,
+ InterfaceModel interfaceModel,
UniqueNameBuilder uniqueNames)
{
- var parameterTypesExpression = GenerateTypeParameterExpression(
+ if (interfaceModel.GeneratedRequestBuilding && methodModel.Request.CanGenerateInline)
+ {
+ WriteInlineRefitMethod(source, methodModel, interfaceModel, isTopLevel);
+ return;
+ }
+
+ var cachedTypeParameterFieldName = GenerateTypeParameterField(
source,
methodModel,
uniqueNames);
@@ -235,77 +317,447 @@ private static void WriteRefitMethod(
var isExplicit = methodModel.IsExplicitInterface || !isTopLevel;
WriteMethodOpening(source, methodModel, isExplicit, isExplicit, isAsync);
- var argumentsArrayString = BuildArgumentsArrayLiteral(methodModel);
- var genericString = BuildGenericTypesArgument(methodModel);
var lookupName = StripExplicitInterfacePrefix(methodModel.Name);
- var callExpression = methodModel.ReturnTypeMetadata == ReturnTypeInfo.SyncVoid
- ? "______func(this.Client, ______arguments);"
- : $"{@return}({returnType})______func(this.Client, ______arguments){configureAwait};";
+ source.WriteIndentation();
+ source.Append("var ______arguments = ");
+ AppendArgumentsArrayLiteral(source, methodModel);
+ source.Append(';');
+ source.WriteLine();
+
+ source.WriteIndentation();
+ source.Append("var ______func = requestBuilder.BuildRestResultFuncForMethod(\"");
+ source.Append(lookupName);
+ source.Append("\", ");
+ AppendTypeParameterExpression(source, methodModel.Parameters, cachedTypeParameterFieldName);
+ AppendGenericTypesArgument(source, methodModel);
+ source.Append(" );");
+ source.WriteLine();
+
+ source.WriteLine();
+ if (methodModel.ReturnTypeMetadata == ReturnTypeInfo.SyncVoid)
+ {
+ source.WriteLine("______func(this.Client, ______arguments);");
+ }
+ else
+ {
+ source.WriteIndentation();
+ source.Append(@return);
+ source.Append('(');
+ source.Append(returnType);
+ source.Append(")______func(this.Client, ______arguments)");
+ source.Append(configureAwait);
+ source.Append(';');
+ source.WriteLine();
+ }
+
+ WriteMethodClosing(source);
+ }
+
+ /// Generates a Refit method that constructs the request directly in generated code.
+ /// The source writer to emit to.
+ /// The method model being emitted.
+ /// The interface model being emitted.
+ /// True if directly from the type we're generating for, false for methods found on base interfaces.
+ private static void WriteInlineRefitMethod(
+ SourceWriter source,
+ MethodModel methodModel,
+ InterfaceModel interfaceModel,
+ bool isTopLevel)
+ {
+ var isExplicit = methodModel.IsExplicitInterface || !isTopLevel;
+ WriteMethodOpening(source, methodModel, isExplicit, isExplicit);
+
+ var request = methodModel.Request;
+ var bodyParameter = FindRequestParameter(request, RequestParameterKind.Body);
+ var cancellationTokenExpression = BuildCancellationTokenExpression(request);
+ var bufferBodyExpression = BuildBufferBodyExpression(bodyParameter);
+ source.WriteLine("var ______settings = requestBuilder.Settings;");
source.WriteLine(
- $"""
- var ______arguments = {argumentsArrayString};
- var ______func = requestBuilder.BuildRestResultFuncForMethod("{lookupName}", {parameterTypesExpression}{genericString} );
+ """var ______basePath = this.Client.BaseAddress?.AbsolutePath ?? throw new global::System.InvalidOperationException("BaseAddress must be set on the HttpClient instance");""");
+ source.WriteLine(
+ "______basePath = ______basePath == \"/\" ? string.Empty : ______basePath.TrimEnd('/');");
+ var requestUriExpression =
+ $"new global::System.Uri(______basePath + {ToCSharpStringLiteral(request.Path)}, global::System.UriKind.Relative)";
+ source.WriteLine(
+ $"var ______rq = new global::System.Net.Http.HttpRequestMessage({ToHttpMethodExpression(request.HttpMethod)}, {requestUriExpression});");
+ source.WriteLine(
+ """
+ #if NET6_0_OR_GREATER
+ ______rq.Version = ______settings.Version;
+ ______rq.VersionPolicy = ______settings.VersionPolicy;
+ #endif
+ """);
- {callExpression}
- """);
+ if (bodyParameter is not null)
+ {
+ var streamBodyExpression = BuildStreamBodyExpression(bodyParameter);
+ var serializationMethodExpression = BuildBodySerializationMethodExpression(bodyParameter);
+ var contentExpression =
+ $"""
+ global::Refit.GeneratedRequestRunner.CreateBodyContent<{bodyParameter.Type}>(
+ ______settings,
+ @{bodyParameter.Name},
+ {serializationMethodExpression},
+ {streamBodyExpression})
+ """;
+ source.WriteLine(
+ $"______rq.Content = {contentExpression};");
+ }
+ WriteInlineHeaders(source, request);
+ WriteInlineRequestProperties(source, request, interfaceModel);
+ WriteInlineReturn(source, methodModel, request, bufferBodyExpression, cancellationTokenExpression);
WriteMethodClosing(source);
}
- /// Builds the object[] literal that holds the method's argument values.
+ /// Emits the return statement for an inline generated Refit method.
+ /// The source writer to emit to.
+ /// The method model being emitted.
+ /// The parsed request model.
+ /// The expression indicating whether request content should be buffered.
+ /// The cancellation token expression.
+ private static void WriteInlineReturn(
+ SourceWriter source,
+ MethodModel methodModel,
+ RequestModel request,
+ string bufferBodyExpression,
+ string cancellationTokenExpression)
+ {
+ if (methodModel.ReturnTypeMetadata == ReturnTypeInfo.AsyncVoid)
+ {
+ var sendVoidExpression =
+ $"""
+ global::Refit.GeneratedRequestRunner.SendVoidAsync(
+ this.Client,
+ ______rq,
+ ______settings,
+ {bufferBodyExpression},
+ {cancellationTokenExpression})
+ """;
+ source.WriteLine(
+ $"return {sendVoidExpression};");
+ return;
+ }
+
+ var sendExpression =
+ $"""
+ global::Refit.GeneratedRequestRunner.SendAsync<{request.ResultType}, {request.DeserializedResultType}>(
+ this.Client,
+ ______rq,
+ ______settings,
+ {ToLowerInvariantString(request.IsApiResponse)},
+ {ToLowerInvariantString(request.ShouldDisposeResponse)},
+ {bufferBodyExpression},
+ {cancellationTokenExpression})
+ """;
+
+ if (methodModel.ReturnType.StartsWith("global::System.Threading.Tasks.ValueTask<", StringComparison.Ordinal))
+ {
+ source.WriteLine($"return new {methodModel.ReturnType}({sendExpression});");
+ return;
+ }
+
+ source.WriteLine($"return {sendExpression};");
+ }
+
+ /// Emits static and dynamic header application for an inline generated method.
+ /// The source writer to emit to.
+ /// The parsed request model.
+ private static void WriteInlineHeaders(SourceWriter source, RequestModel request)
+ {
+ foreach (var header in request.StaticHeaders)
+ {
+ source.WriteLine(
+ $"global::Refit.GeneratedRequestRunner.SetHeader(______rq, {ToCSharpStringLiteral(header.Name)}, "
+ + $"{ToNullableCSharpStringLiteral(header.Value)});");
+ }
+
+ foreach (var parameter in request.Parameters)
+ {
+ switch (parameter.Kind)
+ {
+ case RequestParameterKind.Header:
+ {
+ source.WriteLine(
+ $"global::Refit.GeneratedRequestRunner.SetHeader(______rq, {ToCSharpStringLiteral(parameter.HeaderName)}, "
+ + $"{BuildHeaderValueExpression(parameter)});");
+ continue;
+ }
+
+ case RequestParameterKind.HeaderCollection:
+ {
+ source.WriteLine(
+ $"global::Refit.GeneratedRequestRunner.AddHeaderCollection(______rq, @{parameter.Name});");
+ break;
+ }
+ }
+ }
+ }
+
+ /// Builds a header value expression without null-conditionals on non-nullable value types.
+ /// The header parameter to format.
+ /// The generated header value expression.
+ private static string BuildHeaderValueExpression(RequestParameterModel parameter)
+ {
+ var parameterExpression = $"@{parameter.Name}";
+ return parameter.CanBeNull
+ ? $"{parameterExpression}?.ToString()"
+ : $"{parameterExpression}.ToString()";
+ }
+
+ /// Emits request-option/property application for an inline generated method.
+ /// The source writer to emit to.
+ /// The parsed request model.
+ /// The interface model being emitted.
+ private static void WriteInlineRequestProperties(
+ SourceWriter source,
+ RequestModel request,
+ InterfaceModel interfaceModel)
+ {
+ source.WriteLine(
+ "global::Refit.GeneratedRequestRunner.AddConfiguredRequestOptions(______rq, ______settings, "
+ + $"typeof({interfaceModel.InterfaceDisplayName}));");
+
+ foreach (var property in interfaceModel.Properties)
+ {
+ if (property.RequestPropertyKey.Length == 0 || !property.HasGetter)
+ {
+ continue;
+ }
+
+ source.WriteLine(
+ $"global::Refit.GeneratedRequestRunner.AddRequestProperty<{property.Type}>"
+ + $"(______rq, {ToCSharpStringLiteral(property.RequestPropertyKey)}, "
+ + $"{BuildPropertyAccessExpression(property)});");
+ }
+
+ foreach (var parameter in request.Parameters)
+ {
+ if (parameter.Kind == RequestParameterKind.Property)
+ {
+ source.WriteLine(
+ $"global::Refit.GeneratedRequestRunner.AddRequestProperty<{parameter.Type}>"
+ + $"(______rq, {ToCSharpStringLiteral(parameter.PropertyKey)}, @{parameter.Name});");
+ }
+ }
+ }
+
+ /// Finds the first request parameter of the given kind.
+ /// The request model to inspect.
+ /// The parameter kind to find.
+ /// The parameter model, if present.
+ private static RequestParameterModel? FindRequestParameter(RequestModel request, RequestParameterKind kind)
+ {
+ foreach (var parameter in request.Parameters)
+ {
+ if (parameter.Kind == kind)
+ {
+ return parameter;
+ }
+ }
+
+ return null;
+ }
+
+ /// Builds the cancellation token expression for an inline generated method.
+ /// The request model to inspect.
+ /// The cancellation token expression.
+ private static string BuildCancellationTokenExpression(RequestModel request)
+ {
+ var cancellationToken = FindRequestParameter(request, RequestParameterKind.CancellationToken);
+ if (cancellationToken is null)
+ {
+ return "global::System.Threading.CancellationToken.None";
+ }
+
+ return cancellationToken.Type.StartsWith("global::System.Nullable<", StringComparison.Ordinal)
+ ? $"@{cancellationToken.Name}.GetValueOrDefault()"
+ : $"@{cancellationToken.Name}";
+ }
+
+ /// Builds the request-body buffering expression for an inline generated method.
+ /// The parsed body parameter, if any.
+ /// The buffering expression.
+ private static string BuildBufferBodyExpression(RequestParameterModel? bodyParameter) =>
+ bodyParameter is null
+ ? FalseLiteral
+ : bodyParameter.BodyBufferMode switch
+ {
+ BodyBufferMode.Settings => "______settings.Buffered",
+ BodyBufferMode.Buffered => TrueLiteral,
+ _ => FalseLiteral
+ };
+
+ /// Builds the serialized-body streaming expression for an inline generated method.
+ /// The parsed body parameter.
+ /// The streaming expression.
+ private static string BuildStreamBodyExpression(RequestParameterModel bodyParameter) =>
+ bodyParameter.BodySerializationMethod == "UrlEncoded"
+ ? FalseLiteral
+ : bodyParameter.BodyBufferMode switch
+ {
+ BodyBufferMode.Settings => "!______settings.Buffered",
+ BodyBufferMode.Buffered => FalseLiteral,
+ BodyBufferMode.Streaming => TrueLiteral,
+ _ => FalseLiteral
+ };
+
+ /// Builds the body serialization enum expression for an inline generated method.
+ /// The parsed body parameter.
+ /// The serialization method expression.
+ private static string BuildBodySerializationMethodExpression(RequestParameterModel bodyParameter)
+ {
+ var serializationMethod = bodyParameter.BodySerializationMethod == "Json"
+ ? "Serialized"
+ : bodyParameter.BodySerializationMethod;
+ return $"global::Refit.BodySerializationMethod.{serializationMethod}";
+ }
+
+ /// Builds the expression used to read an implemented interface property.
+ /// The property model.
+ /// The generated property access expression.
+ private static string BuildPropertyAccessExpression(InterfacePropertyModel property)
+ {
+ if (property.IsSatisfiedByGeneratedMember)
+ {
+ return "this.Client";
+ }
+
+ return property.IsExplicitInterface
+ ? $"(({EnsureGlobalPrefix(property.ContainingType)})this).{property.Name}"
+ : $"this.{property.Name}";
+ }
+
+ /// Ensures a type display name is prefixed with global::.
+ /// The type display name.
+ /// The globally qualified type display name.
+ private static string EnsureGlobalPrefix(string typeName) =>
+ typeName.StartsWith(GlobalPrefix, StringComparison.Ordinal)
+ ? typeName
+ : GlobalPrefix + typeName;
+
+ /// Maps a parsed HTTP method name to an expression that creates or returns an .
+ /// The HTTP method text.
+ /// The HTTP method expression.
+ private static string ToHttpMethodExpression(string httpMethod) =>
+ httpMethod switch
+ {
+ "DELETE" => "global::System.Net.Http.HttpMethod.Delete",
+ "GET" => "global::System.Net.Http.HttpMethod.Get",
+ "HEAD" => "global::System.Net.Http.HttpMethod.Head",
+ "OPTIONS" => "global::System.Net.Http.HttpMethod.Options",
+ "POST" => "global::System.Net.Http.HttpMethod.Post",
+ "PUT" => "global::System.Net.Http.HttpMethod.Put",
+ "PATCH" => "new global::System.Net.Http.HttpMethod(\"PATCH\")",
+ _ => throw new ArgumentOutOfRangeException(nameof(httpMethod), httpMethod, "Unsupported HTTP method.")
+ };
+
+ /// Converts a bool to a lowercase C# literal.
+ /// The value to convert.
+ /// The lowercase bool literal.
+ private static string ToLowerInvariantString(bool value) => value ? TrueLiteral : FalseLiteral;
+
+ /// Converts a string into a C# string literal.
+ /// The value to quote.
+ /// The escaped C# string literal.
+ private static string ToCSharpStringLiteral(string value)
+ {
+ var builder = new StringBuilder(value.Length + 2);
+ builder.Append('"');
+ foreach (var c in value)
+ {
+ AppendEscapedCharacter(builder, c);
+ }
+
+ builder.Append('"');
+ return builder.ToString();
+ }
+
+ /// Converts a nullable string into a C# string literal or null literal.
+ /// The value to quote.
+ /// The generated expression.
+ private static string ToNullableCSharpStringLiteral(string? value) =>
+ value is null ? "null" : ToCSharpStringLiteral(value);
+
+ /// Appends one escaped C# string-literal character.
+ /// The target builder.
+ /// The character to append.
+ [SuppressMessage(
+ "CodeQuality",
+ "S1541:Methods and properties should not be too complex",
+ Justification = "A compact switch avoids a dictionary or repeated helper calls on the generator hot path.")]
+ private static void AppendEscapedCharacter(StringBuilder builder, char character) =>
+ _ = character switch
+ {
+ '\\' => builder.Append(@"\\"),
+ '"' => builder.Append("\\\""),
+ '\0' => builder.Append(@"\0"),
+ '\a' => builder.Append(@"\a"),
+ '\b' => builder.Append(@"\b"),
+ '\f' => builder.Append(@"\f"),
+ '\n' => builder.Append(@"\n"),
+ '\r' => builder.Append(@"\r"),
+ '\t' => builder.Append(@"\t"),
+ '\v' => builder.Append(@"\v"),
+ _ => builder.Append(character)
+ };
+
+ /// Appends the object[] literal that holds the method's argument values.
+ /// The source writer to append to.
/// The method model being emitted.
- /// The arguments array expression.
- private static string BuildArgumentsArrayLiteral(MethodModel methodModel)
+ private static void AppendArgumentsArrayLiteral(SourceWriter source, MethodModel methodModel)
{
// Build the arguments array literal directly. This runs for every Refit method, so we
// avoid LINQ Select/ToArray + string.Join and their intermediate array/iterator allocations.
var parameters = methodModel.Parameters.AsArray();
if (parameters.Length == 0)
{
- return "global::System.Array.Empty