Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion InterfaceStubGenerator.Shared/Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,8 @@
ReturnTypeInfo.AsyncVoid => (true, "await (", ").ConfigureAwait(false)"),
ReturnTypeInfo.AsyncResult => (true, "return await (", ").ConfigureAwait(false)"),
ReturnTypeInfo.Return => (false, "return ", ""),
ReturnTypeInfo.SyncVoid => (false, "", ""),
_ => throw new ArgumentOutOfRangeException(

Check warning on line 192 in InterfaceStubGenerator.Shared/Emitter.cs

View workflow job for this annotation

GitHub Actions / build / build-windows

Method WriteRefitMethod passes 'ReturnTypeMetadata' as the paramName argument to a ArgumentOutOfRangeException constructor. Replace this argument with one of the method's parameter names. Note that the provided parameter name should have the exact casing as declared on the method. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2208)

Check warning on line 192 in InterfaceStubGenerator.Shared/Emitter.cs

View workflow job for this annotation

GitHub Actions / build / build-unix (ubuntu-latest)

Method WriteRefitMethod passes 'ReturnTypeMetadata' as the paramName argument to a ArgumentOutOfRangeException constructor. Replace this argument with one of the method's parameter names. Note that the provided parameter name should have the exact casing as declared on the method. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2208)
nameof(methodModel.ReturnTypeMetadata),
methodModel.ReturnTypeMetadata,
"Unsupported value."
Expand Down Expand Up @@ -228,12 +229,16 @@
lookupName = lookupName.Substring(lastDotIndex + 1);
}

var callExpression = methodModel.ReturnTypeMetadata == ReturnTypeInfo.SyncVoid
? $"______func(this.Client, ______arguments);"
: $"{@return}({returnType})______func(this.Client, ______arguments){configureAwait};";

source.WriteLine(
$"""
var ______arguments = {argumentsArrayString};
var ______func = requestBuilder.BuildRestResultFuncForMethod("{lookupName}", {parameterTypesExpression}{genericString} );

{@return}({returnType})______func(this.Client, ______arguments){configureAwait};
{callExpression}
"""
);

Expand Down Expand Up @@ -317,7 +322,7 @@
if (isExplicitInterface)
{
var ct = methodModel.ContainingType;
if (!ct.StartsWith("global::"))

Check warning on line 325 in InterfaceStubGenerator.Shared/Emitter.cs

View workflow job for this annotation

GitHub Actions / build / build-windows

The behavior of 'string.StartsWith(string)' could vary based on the current user's locale settings. Replace this call in 'Refit.Generator.Emitter.WriteMethodOpening(Refit.Generator.SourceWriter, Refit.Generator.MethodModel, bool, bool, bool)' with a call to 'string.StartsWith(string, System.StringComparison)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310)

Check warning on line 325 in InterfaceStubGenerator.Shared/Emitter.cs

View workflow job for this annotation

GitHub Actions / build / build-unix (ubuntu-latest)

The behavior of 'string.StartsWith(string)' could vary based on the current user's locale settings. Replace this call in 'Refit.Generator.Emitter.WriteMethodOpening(Refit.Generator.SourceWriter, Refit.Generator.MethodModel, bool, bool, bool)' with a call to 'string.StartsWith(string, System.StringComparison)'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1310)
{
ct = "global::" + ct;
}
Expand All @@ -337,7 +342,7 @@
builder.Append(string.Join(", ", list));
}

builder.Append(")");

Check warning on line 345 in InterfaceStubGenerator.Shared/Emitter.cs

View workflow job for this annotation

GitHub Actions / build / build-windows

Use 'StringBuilder.Append(char)' instead of 'StringBuilder.Append(string)' when the input is a constant unit string (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1834)

Check warning on line 345 in InterfaceStubGenerator.Shared/Emitter.cs

View workflow job for this annotation

GitHub Actions / build / build-unix (ubuntu-latest)

Use 'StringBuilder.Append(char)' instead of 'StringBuilder.Append(string)' when the input is a constant unit string (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1834)

source.WriteLine();
source.WriteLine(builder.ToString());
Expand Down
3 changes: 2 additions & 1 deletion InterfaceStubGenerator.Shared/Models/MethodModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ internal enum ReturnTypeInfo : byte
{
Return,
AsyncVoid,
AsyncResult
AsyncResult,
SyncVoid
}
2 changes: 2 additions & 0 deletions InterfaceStubGenerator.Shared/Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
// TODO: we should allow source generators to provide source during initialize, so that this step isn't required.
var options = (CSharpParseOptions)compilation.SyntaxTrees[0].Options;

var disposableInterfaceSymbol = wellKnownTypes.Get(typeof(IDisposable));

Check warning on line 46 in InterfaceStubGenerator.Shared/Parser.cs

View workflow job for this annotation

GitHub Actions / build / build-windows

Prefer the generic overload 'Refit.Generator.WellKnownTypes.Get<T>()' instead of 'Refit.Generator.WellKnownTypes.Get(System.Type)' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2263)

Check warning on line 46 in InterfaceStubGenerator.Shared/Parser.cs

View workflow job for this annotation

GitHub Actions / build / build-unix (ubuntu-latest)

Prefer the generic overload 'Refit.Generator.WellKnownTypes.Get<T>()' instead of 'Refit.Generator.WellKnownTypes.Get(System.Type)' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2263)
var httpMethodBaseAttributeSymbol = wellKnownTypes.TryGet(
"Refit.HttpMethodAttribute"
);
Expand Down Expand Up @@ -462,6 +462,7 @@
{
"Task" => ReturnTypeInfo.AsyncVoid,
"Task`1" or "ValueTask`1" => ReturnTypeInfo.AsyncResult,
"Void" => ReturnTypeInfo.SyncVoid,
_ => ReturnTypeInfo.Return,
};

Expand Down Expand Up @@ -623,6 +624,7 @@
{
"Task" => ReturnTypeInfo.AsyncVoid,
"Task`1" or "ValueTask`1" => ReturnTypeInfo.AsyncResult,
"Void" => ReturnTypeInfo.SyncVoid,
_ => ReturnTypeInfo.Return,
};

Expand Down
7 changes: 3 additions & 4 deletions Refit.GeneratorTests/Refit.GeneratorTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,20 @@
<IsTestProject>true</IsTestProject>
<NoWarn>$(NoWarn);CS1591;CA1819;CA2000;CA2007;CA1056;CA1707;CA1861;xUnit1031</NoWarn>
</PropertyGroup>

<ItemGroup Condition="$(TargetFramework.StartsWith('net9.0'))">
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="9.0.*" />
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.StartsWith('net8.0'))">
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="8.0.*" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.Formats.Asn1" Version="9.0.*" />
<PackageReference Include="coverlet.collector" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageReference Include="System.Collections.Immutable" Version="9.0.*" />
<PackageReference Include="Verify.DiffPlex" Version="3.1.2" />
<PackageReference Include="Verify.SourceGenerators" Version="2.5.0" />
<PackageReference Include="Verify.Xunit" Version="31.12.5" />
Expand All @@ -35,7 +34,7 @@
<ProjectReference Include="..\Refit.Newtonsoft.Json\Refit.Newtonsoft.Json.csproj" />
<ProjectReference Include="..\Refit.Xml\Refit.Xml.csproj" />
<ProjectReference Include="..\InterfaceStubGenerator.Roslyn38\InterfaceStubGenerator.Roslyn38.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="true" />
<ProjectReference Include="..\InterfaceStubGenerator.Roslyn41\InterfaceStubGenerator.Roslyn41.csproj" />
<ProjectReference Include="..\InterfaceStubGenerator.Roslyn41\InterfaceStubGenerator.Roslyn41.csproj" OutputItemType="Analyzer" />
<ProjectReference Include="..\Refit\Refit.csproj" />
</ItemGroup>

Expand Down
28 changes: 28 additions & 0 deletions Refit.Tests/AuthenticatedClientHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,34 @@ public async Task AuthorizationHeaderValueGetterIsUsedWhenSupplyingHttpClient()
Assert.Equal("Ok", result);
}

[Fact]
public async Task AuthorizationHeaderValueGetterCanAwaitWhenSupplyingHttpClient()
{
var handler = new MockHttpMessageHandler();
var httpClient = new HttpClient(handler) { BaseAddress = new Uri("http://api") };

var settings = new RefitSettings
{
AuthorizationHeaderValueGetter = async (_, __) =>
{
await Task.Yield();
return "tokenValue";
}
};

handler
.Expect(HttpMethod.Get, "http://api/auth")
.WithHeaders("Authorization", "Bearer tokenValue")
.Respond("text/plain", "Ok");

var fixture = RestService.For<IMyAuthenticatedService>(httpClient, settings);

var result = await fixture.GetAuthenticated();

handler.VerifyNoOutstandingExpectation();
Assert.Equal("Ok", result);
}

[Fact]
public async Task AuthorizationHeaderValueGetterDoesNotOverrideExplicitTokenWhenSupplyingHttpClient()
{
Expand Down
194 changes: 191 additions & 3 deletions Refit.Tests/ExplicitInterfaceRefitTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net.Http;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Refit;
using RichardSzalay.MockHttp;
Expand All @@ -8,6 +9,12 @@ namespace Refit.Tests;

public class ExplicitInterfaceRefitTests
{
sealed class SyncCapableMockHttpMessageHandler : MockHttpMessageHandler
{
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) =>
SendAsync(request, cancellationToken).GetAwaiter().GetResult();
}

public interface IFoo
{
int Bar();
Expand All @@ -29,10 +36,35 @@ public interface IRemoteFoo2 : IFoo
abstract int IFoo.Bar();
}

// Interfaces used to test the full sync pipeline
public interface ISyncPipelineApi
{
[Get("/resource")]
internal string GetString();

[Get("/resource")]
internal HttpResponseMessage GetHttpResponseMessage();

[Get("/resource")]
internal HttpContent GetHttpContent();

[Get("/resource")]
internal Stream GetStream();

[Get("/resource")]
internal IApiResponse<string> GetApiResponse();

[Get("/resource")]
internal IApiResponse GetRawApiResponse();

[Get("/resource")]
internal void DoVoid();
}

[Fact]
public void DefaultInterfaceImplementation_calls_internal_refit_method()
{
var mockHttp = new MockHttpMessageHandler();
var mockHttp = new SyncCapableMockHttpMessageHandler();
var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp };

mockHttp
Expand All @@ -50,7 +82,7 @@ public void DefaultInterfaceImplementation_calls_internal_refit_method()
[Fact]
public void Explicit_interface_member_with_refit_attribute_is_invoked()
{
var mockHttp = new MockHttpMessageHandler();
var mockHttp = new SyncCapableMockHttpMessageHandler();
var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp };

mockHttp
Expand All @@ -64,4 +96,160 @@ public void Explicit_interface_member_with_refit_attribute_is_invoked()

mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public void Sync_method_throws_ApiException_on_error_response()
{
var mockHttp = new SyncCapableMockHttpMessageHandler();
var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp };

mockHttp
.Expect(HttpMethod.Get, "http://foo/resource")
.Respond(HttpStatusCode.NotFound);

var fixture = RestService.For<ISyncPipelineApi>("http://foo", settings);

var ex = Assert.Throws<ApiException>(() => fixture.GetString());
Assert.Equal(HttpStatusCode.NotFound, ex.StatusCode);

mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public void Sync_method_returns_HttpResponseMessage_without_running_ExceptionFactory()
{
var mockHttp = new SyncCapableMockHttpMessageHandler();
var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp };

mockHttp
.Expect(HttpMethod.Get, "http://foo/resource")
.Respond(HttpStatusCode.NotFound);

var fixture = RestService.For<ISyncPipelineApi>("http://foo", settings);

// Should not throw even for a 404 – caller owns the response
using var resp = fixture.GetHttpResponseMessage();
Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode);

mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public void Sync_method_returns_HttpContent_without_disposing_response()
{
var mockHttp = new SyncCapableMockHttpMessageHandler();
var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp };

mockHttp
.Expect(HttpMethod.Get, "http://foo/resource")
.Respond("text/plain", "hello");

var fixture = RestService.For<ISyncPipelineApi>("http://foo", settings);

var content = fixture.GetHttpContent();
Assert.NotNull(content);
var text = content.ReadAsStringAsync().GetAwaiter().GetResult();
Assert.Equal("hello", text);

mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public void Sync_method_returns_Stream_without_disposing_response()
{
var mockHttp = new SyncCapableMockHttpMessageHandler();
var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp };

mockHttp
.Expect(HttpMethod.Get, "http://foo/resource")
.Respond("text/plain", "hello");

var fixture = RestService.For<ISyncPipelineApi>("http://foo", settings);

using var stream = fixture.GetStream();
Assert.NotNull(stream);
using var reader = new StreamReader(stream);
Assert.Equal("hello", reader.ReadToEnd());

mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public void Sync_method_returns_IApiResponse_with_error_on_bad_status()
{
var mockHttp = new SyncCapableMockHttpMessageHandler();
var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp };

mockHttp
.Expect(HttpMethod.Get, "http://foo/resource")
.Respond(HttpStatusCode.InternalServerError);

var fixture = RestService.For<ISyncPipelineApi>("http://foo", settings);

using var apiResp = fixture.GetApiResponse();
Assert.False(apiResp.IsSuccessStatusCode);
Assert.NotNull(apiResp.Error);
Assert.True(apiResp.HasResponseError(out var error));
Assert.Equal(HttpStatusCode.InternalServerError, error.StatusCode);

mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public void Sync_method_returns_IApiResponse_with_content_on_success()
{
var mockHttp = new SyncCapableMockHttpMessageHandler();
var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp };

mockHttp
.Expect(HttpMethod.Get, "http://foo/resource")
.Respond("application/json", "\"hello\"");

var fixture = RestService.For<ISyncPipelineApi>("http://foo", settings);

using var apiResp = fixture.GetApiResponse();
Assert.True(apiResp.IsSuccessStatusCode);
Assert.Null(apiResp.Error);
Assert.Equal(HttpMethod.Get, apiResp.RequestMessage.Method);
Assert.Equal("http://foo/resource", apiResp.RequestMessage.RequestUri?.ToString());
// The string branch reads the raw stream (no JSON unwrapping), same as the async path
Assert.Equal("\"hello\"", apiResp.Content);

mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public void Sync_void_method_throws_ApiException_on_error_response()
{
var mockHttp = new SyncCapableMockHttpMessageHandler();
var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp };

mockHttp
.Expect(HttpMethod.Get, "http://foo/resource")
.Respond(HttpStatusCode.BadRequest);

var fixture = RestService.For<ISyncPipelineApi>("http://foo", settings);

var ex = Assert.Throws<ApiException>(() => fixture.DoVoid());
Assert.Equal(HttpStatusCode.BadRequest, ex.StatusCode);

mockHttp.VerifyNoOutstandingExpectation();
}

[Fact]
public void Sync_void_method_succeeds_on_ok_response()
{
var mockHttp = new SyncCapableMockHttpMessageHandler();
var settings = new RefitSettings { HttpMessageHandlerFactory = () => mockHttp };

mockHttp
.Expect(HttpMethod.Get, "http://foo/resource")
.Respond(HttpStatusCode.OK);

var fixture = RestService.For<ISyncPipelineApi>("http://foo", settings);

fixture.DoVoid(); // should not throw

mockHttp.VerifyNoOutstandingExpectation();
}
}
31 changes: 31 additions & 0 deletions Refit.Tests/RequestBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2443,6 +2443,37 @@ public void StreamResponseAsApiResponseTest()
Assert.Equal(reponseContent, reader.ReadToEnd());
}

[Fact]
public void GeneratedSyncApiResponseShouldPreserveRequestMessage()
{
var fixture = new RequestBuilderImplementation<IDummyHttpApi>();
var restMethod = new RestMethodInfoInternal(
typeof(IDummyHttpApi),
typeof(IDummyHttpApi)
.GetMethods()
.First(x => x.Name == nameof(IDummyHttpApi.FetchSomeStringWithMetadata))
);
var buildGeneratedSyncFuncForMethod = typeof(RequestBuilderImplementation).GetMethod(
"BuildGeneratedSyncFuncForMethod",
BindingFlags.Instance | BindingFlags.NonPublic
);
var factory = (Func<HttpClient, object[], object?>)
buildGeneratedSyncFuncForMethod!.Invoke(fixture, [restMethod])!;
var testHttpMessageHandler = new TestHttpMessageHandler();

var response = (ApiResponse<string>)
factory(
new HttpClient(testHttpMessageHandler)
{
BaseAddress = new Uri("http://api/")
},
[42]
)!;

Assert.Same(testHttpMessageHandler.RequestMessage, response.RequestMessage);
Assert.Equal(testHttpMessageHandler.RequestMessage.RequestUri, response.RequestMessage.RequestUri);
}

[Fact]
public void StreamResponseTest()
{
Expand Down
Loading
Loading