Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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)
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)
{
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)

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)
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
192 changes: 189 additions & 3 deletions Refit.Tests/ExplicitInterfaceRefitTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Net.Http;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
Comment thread
ChrisPulman marked this conversation as resolved.
Outdated
using System.Threading.Tasks;
using Refit;
using RichardSzalay.MockHttp;
Expand All @@ -8,6 +10,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 +37,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 +83,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 +97,157 @@ 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.Equal(HttpStatusCode.InternalServerError, apiResp.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);
// 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();
}
}
Loading
Loading