Skip to content

Commit df6f31c

Browse files
Merge pull request #1031 from reactiveui/default-interface-methods
Add support for internal and non-abstract interface members
2 parents f6e30cf + 948857a commit df6f31c

4 files changed

Lines changed: 162 additions & 3 deletions

File tree

InterfaceStubGenerator.Core/InterfaceStubGenerator.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,10 +205,14 @@ partial class AutoGenerated{classDeclaration}
205205
ProcessRefitMethod(source, method);
206206
}
207207

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

214218
ProcessNonRefitMethod(source, method, context);

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ services.AddRefitClient<IGitHubApi>("https://api.github.com");
6161
* [Using generic interfaces](#using-generic-interfaces)
6262
* [Interface inheritance](#interface-inheritance)
6363
* [Headers inheritance](#headers-inheritance)
64+
* [Default Interface Methods](#default-interface-methods)
6465
* [Using HttpClientFactory](#using-httpclientfactory)
6566
* [Handling exceptions](#handling-exceptions)
6667
* [MSBuild configuration](#msbuild-configuration)
@@ -974,6 +975,46 @@ public interface IAmInterfaceC : IAmInterfaceA, IAmInterfaceB
974975

975976
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.
976977

978+
### Default Interface Methods
979+
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:
980+
```csharp
981+
public interface IApiClient
982+
{
983+
// implemented by Refit but not exposed publicly
984+
[Get("/get")]
985+
internal Task<string> GetInternal();
986+
// Publicly available with added logic applied to the result from the API call
987+
public async Task<string> Get()
988+
=> FormatResponse(await GetInternal());
989+
private static String FormatResponse(string response)
990+
=> $"The response is: {response}";
991+
}
992+
```
993+
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`.
994+
The default interface method `IApiClient.Get` will be inherited by all types implementing `IApiClient`, including - of course - the type generated by Refit.
995+
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`).
996+
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.
997+
```csharp
998+
internal interface IApiClientInternal
999+
{
1000+
[Get("/get")]
1001+
Task<string> Get();
1002+
}
1003+
public interface IApiClient
1004+
{
1005+
public Task<string> Get();
1006+
}
1007+
internal class ApiClient : IApiClient
1008+
{
1009+
private readonly IApiClientInternal client;
1010+
public ApiClient(IApiClientInternal client) => this.client = client;
1011+
public async Task<string> Get()
1012+
=> FormatResponse(await client.Get());
1013+
private static String FormatResponse(string response)
1014+
=> $"The response is: {response}";
1015+
}
1016+
```
1017+
9771018
### Using HttpClientFactory
9781019

9791020
Refit has first class support for the ASP.Net Core 2.1 HttpClientFactory. Add a reference to `Refit.HttpClientFactory` and call
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Net.Http;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
using RichardSzalay.MockHttp;
10+
11+
using Xunit;
12+
13+
namespace Refit.Tests
14+
{
15+
public interface IHaveDims
16+
{
17+
[Get("")]
18+
internal Task<string> GetInternal();
19+
20+
// DIMs require C# 8.0 which requires .NET Core 3.x or .NET Standard 2.1
21+
#if NETCOREAPP3_1_OR_GREATER
22+
private Task<string> GetPrivate()
23+
{
24+
return GetInternal();
25+
}
26+
27+
Task<string> GetDim()
28+
{
29+
return GetPrivate();
30+
}
31+
32+
static string GetStatic()
33+
{
34+
return nameof(IHaveDims);
35+
}
36+
#endif
37+
}
38+
39+
// DIMs require C# 8.0 which requires .NET Core 3.x or .NET Standard 2.1
40+
#if NETCOREAPP3_1_OR_GREATER
41+
public class DefaultInterfaceMethodTests
42+
{
43+
[Fact]
44+
public async Task InternalInterfaceMemberTest()
45+
{
46+
var mockHttp = new MockHttpMessageHandler();
47+
48+
var settings = new RefitSettings
49+
{
50+
HttpMessageHandlerFactory = () => mockHttp
51+
};
52+
53+
mockHttp.Expect(HttpMethod.Get, "https://httpbin.org/")
54+
.Respond(HttpStatusCode.OK, "text/html", "OK");
55+
56+
var fixture = RestService.For<IHaveDims>("https://httpbin.org/", settings);
57+
var plainText = await fixture.GetInternal();
58+
59+
Assert.True(!string.IsNullOrWhiteSpace(plainText));
60+
}
61+
62+
[Fact]
63+
public async Task DimTest()
64+
{
65+
var mockHttp = new MockHttpMessageHandler();
66+
67+
var settings = new RefitSettings
68+
{
69+
HttpMessageHandlerFactory = () => mockHttp
70+
};
71+
72+
mockHttp.Expect(HttpMethod.Get, "https://httpbin.org/")
73+
.Respond(HttpStatusCode.OK, "text/html", "OK");
74+
75+
var fixture = RestService.For<IHaveDims>("https://httpbin.org/", settings);
76+
var plainText = await fixture.GetDim();
77+
78+
Assert.True(!string.IsNullOrWhiteSpace(plainText));
79+
}
80+
81+
[Fact]
82+
public async Task InternalDimTest()
83+
{
84+
var mockHttp = new MockHttpMessageHandler();
85+
86+
var settings = new RefitSettings
87+
{
88+
HttpMessageHandlerFactory = () => mockHttp
89+
};
90+
91+
mockHttp.Expect(HttpMethod.Get, "https://httpbin.org/")
92+
.Respond(HttpStatusCode.OK, "text/html", "OK");
93+
94+
var fixture = RestService.For<IHaveDims>("https://httpbin.org/", settings);
95+
var plainText = await fixture.GetInternal();
96+
97+
Assert.True(!string.IsNullOrWhiteSpace(plainText));
98+
}
99+
100+
[Fact]
101+
public void StaticInterfaceMethodTest()
102+
{
103+
var plainText = IHaveDims.GetStatic();
104+
105+
Assert.True(!string.IsNullOrWhiteSpace(plainText));
106+
}
107+
}
108+
#endif
109+
}

Refit/RequestBuilderImplementation.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,12 @@ public RequestBuilderImplementation(Type refitInterfaceType, RefitSettings? refi
6363

6464
void AddInterfaceHttpMethods(Type interfaceType, Dictionary<string, List<RestMethodInfo>> methods)
6565
{
66-
foreach (var methodInfo in interfaceType.GetMethods())
66+
// Consider public (the implicit visibility) and non-public abstract members of the interfaceType
67+
var methodInfos = interfaceType
68+
.GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
69+
.Where(i => i.IsAbstract);
70+
71+
foreach (var methodInfo in methodInfos)
6772
{
6873
var attrs = methodInfo.GetCustomAttributes(true);
6974
var hasHttpMethod = attrs.OfType<HttpMethodAttribute>().Any();

0 commit comments

Comments
 (0)