diff --git a/InterfaceStubGenerator.Core/InterfaceStubGenerator.cs b/InterfaceStubGenerator.Core/InterfaceStubGenerator.cs index ec2f4c72b..8165b8f16 100644 --- a/InterfaceStubGenerator.Core/InterfaceStubGenerator.cs +++ b/InterfaceStubGenerator.Core/InterfaceStubGenerator.cs @@ -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); diff --git a/README.md b/README.md index 4747c56ee..b9f5937dd 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ services.AddRefitClient("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) @@ -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 GetInternal(); + // Publicly available with added logic applied to the result from the API call + public async Task 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 Get(); +} +public interface IApiClient +{ + public Task Get(); +} +internal class ApiClient : IApiClient +{ + private readonly IApiClientInternal client; + public ApiClient(IApiClientInternal client) => this.client = client; + public async Task 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 diff --git a/Refit.Tests/IDefaultInterfaceMethodTests.cs b/Refit.Tests/IDefaultInterfaceMethodTests.cs new file mode 100644 index 000000000..cdfca77f3 --- /dev/null +++ b/Refit.Tests/IDefaultInterfaceMethodTests.cs @@ -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 GetInternal(); + + // DIMs require C# 8.0 which requires .NET Core 3.x or .NET Standard 2.1 +#if NETCOREAPP3_1_OR_GREATER + private Task GetPrivate() + { + return GetInternal(); + } + + Task 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("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("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("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 +} diff --git a/Refit/RequestBuilderImplementation.cs b/Refit/RequestBuilderImplementation.cs index b276058f1..56a24b8bc 100644 --- a/Refit/RequestBuilderImplementation.cs +++ b/Refit/RequestBuilderImplementation.cs @@ -63,7 +63,12 @@ public RequestBuilderImplementation(Type refitInterfaceType, RefitSettings? refi void AddInterfaceHttpMethods(Type interfaceType, Dictionary> 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().Any();