Skip to content

Commit

Permalink
Merge pull request #1031 from reactiveui/default-interface-methods
Browse files Browse the repository at this point in the history
Add support for internal and non-abstract interface members
  • Loading branch information
clairernovotny authored Jan 23, 2021
2 parents f6e30cf + 948857a commit df6f31c
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 3 deletions.
8 changes: 6 additions & 2 deletions InterfaceStubGenerator.Core/InterfaceStubGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,14 @@ partial class AutoGenerated{classDeclaration}
ProcessRefitMethod(source, method);
}

// Handle non-refit Methods that aren't static or properties
// Handle non-refit Methods that aren't static or properties or have a method body
foreach(var method in nonRefitMethods.Concat(derivedNonRefitMethods))
{
if (method.IsStatic || method.MethodKind == MethodKind.PropertyGet || method.MethodKind == MethodKind.PropertySet)
if (method.IsStatic ||
method.MethodKind == MethodKind.PropertyGet ||
method.MethodKind == MethodKind.PropertySet ||
(method.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is MethodDeclarationSyntax methodSyntax && methodSyntax.Body is not null)
)
continue;

ProcessNonRefitMethod(source, method, context);
Expand Down
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ services.AddRefitClient<IGitHubApi>("https://api.github.com");
* [Using generic interfaces](#using-generic-interfaces)
* [Interface inheritance](#interface-inheritance)
* [Headers inheritance](#headers-inheritance)
* [Default Interface Methods](#default-interface-methods)
* [Using HttpClientFactory](#using-httpclientfactory)
* [Handling exceptions](#handling-exceptions)
* [MSBuild configuration](#msbuild-configuration)
Expand Down Expand Up @@ -974,6 +975,46 @@ public interface IAmInterfaceC : IAmInterfaceA, IAmInterfaceB

Here `IAmInterfaceC.Foo` would use the header attribute inherited from `IAmInterfaceA`, if present, or the one inherited from `IAmInterfaceB`, and so on for all the declared interfaces.

### Default Interface Methods
Starting with C# 8.0, default interface methods (a.k.a. DIMs) can be defined on interfaces. Refit interfaces can provide additional logic using DIMs, optionally combined with private and/or static helper methods:
```csharp
public interface IApiClient
{
// implemented by Refit but not exposed publicly
[Get("/get")]
internal Task<string> GetInternal();
// Publicly available with added logic applied to the result from the API call
public async Task<string> Get()
=> FormatResponse(await GetInternal());
private static String FormatResponse(string response)
=> $"The response is: {response}";
}
```
The type generated by Refit will implement the method `IApiClient.GetInternal`. If additional logic is required immediately before or after its invocation, it shouldn't be exposed directly and can thus be hidden from consumers by being marked as `internal`.
The default interface method `IApiClient.Get` will be inherited by all types implementing `IApiClient`, including - of course - the type generated by Refit.
Consumers of the `IApiClient` will call the public `Get` method and profit from the additional logic provided in its implementation (optionally, in this case, with the help of the private static helper `FormatResponse`).
To support runtimes without DIM-support (.NET Core 2.x and below or .NET Standard 2.0 and below), two additional types would be required for the same solution.
```csharp
internal interface IApiClientInternal
{
[Get("/get")]
Task<string> Get();
}
public interface IApiClient
{
public Task<string> Get();
}
internal class ApiClient : IApiClient
{
private readonly IApiClientInternal client;
public ApiClient(IApiClientInternal client) => this.client = client;
public async Task<string> Get()
=> FormatResponse(await client.Get());
private static String FormatResponse(string response)
=> $"The response is: {response}";
}
```

### Using HttpClientFactory

Refit has first class support for the ASP.Net Core 2.1 HttpClientFactory. Add a reference to `Refit.HttpClientFactory` and call
Expand Down
109 changes: 109 additions & 0 deletions Refit.Tests/IDefaultInterfaceMethodTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

using RichardSzalay.MockHttp;

using Xunit;

namespace Refit.Tests
{
public interface IHaveDims
{
[Get("")]
internal Task<string> GetInternal();

// DIMs require C# 8.0 which requires .NET Core 3.x or .NET Standard 2.1
#if NETCOREAPP3_1_OR_GREATER
private Task<string> GetPrivate()
{
return GetInternal();
}

Task<string> GetDim()
{
return GetPrivate();
}

static string GetStatic()
{
return nameof(IHaveDims);
}
#endif
}

// DIMs require C# 8.0 which requires .NET Core 3.x or .NET Standard 2.1
#if NETCOREAPP3_1_OR_GREATER
public class DefaultInterfaceMethodTests
{
[Fact]
public async Task InternalInterfaceMemberTest()
{
var mockHttp = new MockHttpMessageHandler();

var settings = new RefitSettings
{
HttpMessageHandlerFactory = () => mockHttp
};

mockHttp.Expect(HttpMethod.Get, "https://httpbin.org/")
.Respond(HttpStatusCode.OK, "text/html", "OK");

var fixture = RestService.For<IHaveDims>("https://httpbin.org/", settings);
var plainText = await fixture.GetInternal();

Assert.True(!string.IsNullOrWhiteSpace(plainText));
}

[Fact]
public async Task DimTest()
{
var mockHttp = new MockHttpMessageHandler();

var settings = new RefitSettings
{
HttpMessageHandlerFactory = () => mockHttp
};

mockHttp.Expect(HttpMethod.Get, "https://httpbin.org/")
.Respond(HttpStatusCode.OK, "text/html", "OK");

var fixture = RestService.For<IHaveDims>("https://httpbin.org/", settings);
var plainText = await fixture.GetDim();

Assert.True(!string.IsNullOrWhiteSpace(plainText));
}

[Fact]
public async Task InternalDimTest()
{
var mockHttp = new MockHttpMessageHandler();

var settings = new RefitSettings
{
HttpMessageHandlerFactory = () => mockHttp
};

mockHttp.Expect(HttpMethod.Get, "https://httpbin.org/")
.Respond(HttpStatusCode.OK, "text/html", "OK");

var fixture = RestService.For<IHaveDims>("https://httpbin.org/", settings);
var plainText = await fixture.GetInternal();

Assert.True(!string.IsNullOrWhiteSpace(plainText));
}

[Fact]
public void StaticInterfaceMethodTest()
{
var plainText = IHaveDims.GetStatic();

Assert.True(!string.IsNullOrWhiteSpace(plainText));
}
}
#endif
}
7 changes: 6 additions & 1 deletion Refit/RequestBuilderImplementation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,12 @@ public RequestBuilderImplementation(Type refitInterfaceType, RefitSettings? refi

void AddInterfaceHttpMethods(Type interfaceType, Dictionary<string, List<RestMethodInfo>> methods)
{
foreach (var methodInfo in interfaceType.GetMethods())
// Consider public (the implicit visibility) and non-public abstract members of the interfaceType
var methodInfos = interfaceType
.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
.Where(i => i.IsAbstract);

foreach (var methodInfo in methodInfos)
{
var attrs = methodInfo.GetCustomAttributes(true);
var hasHttpMethod = attrs.OfType<HttpMethodAttribute>().Any();
Expand Down

0 comments on commit df6f31c

Please sign in to comment.