reactiveui / refit

The automatic type-safe REST library for .NET Core, Xamarin and .NET. Heavily inspired by Square's Retrofit library, Refit turns your REST API into a live interface.
https://reactiveui.github.io/refit/
MIT License
8.47k stars 745 forks source link

feature: C# 9 Source Generator for programmatically generating Refit clients based off controllers #1003

Open james-s-tayler opened 3 years ago

james-s-tayler commented 3 years ago

Is your feature request related to a problem? Please describe. Reading through https://github.com/reactiveui/refit/issues/972 sparked an idea of doing the reverse and creating a C# 9 source generator that outputs Refit clients for controllers marked with [ApiController] in your project. The main use case of this is to support making consuming internal api's easier, and in my particular use case make integration testing much easier. The integration testing story in .NET Core is generally pretty good, and spinning up an in-memory TestServer with a pre-configured HttpClient lets you write integration tests really easily. This works fine for GET requests but PUT/POST are a little unwieldy to work with in raw HttpClient I find, so I always wind up writing Refit clients for them that I can pass the HttpClient to. Automatically generating the Refit client based on the controllers would be a great productivity boost here and a really cool use of Source Generators.

Describe alternatives you've considered Currently I resort to writing them by hand. I have in the past leveraged the swagger/open-api tooling to generate client SDKs and while it's an OK(ish) option for a public SDK you plan to distribute on Nuget, the openapi-generator for .NET Core uses RestSharp under the hood which doesn't support HttpClient, so it's not really acceptable to me and I always come back to Refit. There looks to be a 3rd party generator for openapi that supports Refit, but it's not official and including it some kind of build process would likely be a kludge.

Describe suggestions on how to achieve the feature I haven't looked too deeply into how to go about doing this. Suffice to say it requires writing an ISourceGenerator capable of spitting out the required code. For writing the actual generator I would imagine a combination of looking at the Refit stubs generator code and the Swashbuckle / Swagger code might be a good place to start in terms of trying to piece something together. It seems source generators can be distributed as Nuget packages the same way Rosyln analyzers are.

I'm mainly leaving this as a note to my future self when I have some bandwidth to have a play with implementing it. Will update this with more research as I conduct it.

Edit: saw a StackOverflow post about this too https://stackoverflow.com/questions/52858525/how-can-i-automatically-generate-refit-interfaces-from-existing-controllers

Edit2: a corollary to this feature would be adding Roslyn analyzers to help developers use Refit successfully e.g showing warnings on things like including a [Body] parameter in a [Get] request.

Dreamescaper commented 3 years ago

Unfortunately, it's not really easy.

Same as you, I was dissatisfied with existing generators based on OpenAPI docs, and I've created dotnet tool to generate Refit clients based on ASP.NET Core controllers directly (here).

However, same as Swashbuckle or NSwag, it uses ASP.NET Core's API Explorer to get endpoints metadata. And in order to use that, I need to actually build and run the code, which is not that fast.

Source Generators are suitable in cases when you can get all the info you need from source code only (using Roslyn). And while that's possibly true for controllers in the most basic cases, there's quite a lot of stuff that can change routing rules in runtime. For example - versioning. You have some routing rules, then you configure versioning, and bam - you have another rules.

kfrajtak commented 3 years ago

@Dreamescaper the source of information is not limited to source code only - see the example using CSV file as a source (https://devblogs.microsoft.com/dotnet/new-c-source-generator-samples/), you can use swagger definition file. However, massive amount of work will be required ...

.NET Blog
New C# Source Generator Samples
Phillip introduced C# Source Generators here. This post describes two new generators that we added to the samples project in the Roslyn SDK github repo. The first generator gives you strongly typed access to CSV data. The second one creates string constants based on Mustache specifications.
Dreamescaper commented 3 years ago

Sure, you can use swagger.json file. But in order to get that swagger file (via NSwag or Swashbuckle), you'll need to actually compile and run your code.

ahmednfwela commented 2 years ago

Just a passing thought, why not create an openapi generator using java based on refit ? this generator for example has options to use the libraries httpclient/restsharp I think another library option with refit would be cool

christianhelle commented 1 year ago

You can generate Refit interfaces from OpenAPI specifications using a tool I made called Refitter.

You can do this via the command line using Refitter directly or from Visual Studio using an extension I made called REST API Client Code Generator

Melchy commented 1 year ago

I have created a source generator that can generate strongly typed clients. It's called Ridge. It doesn't generate Refit clients but that shouldn't be a problem.

It can also solve problems described above by @Dreamescaper. There are two tricks to solve the problem - use of WebApplicationFactory and extendability. Ridge can be used only in combination with WebApplicationFactory because it uses internal ASP.NET services to generate request. Namely LinkGenerator.

LinkGenerator is quite powerful in combination with Ridges extendability and I believe you can solve most of the problems (not all) you stumble upon when generating requests.

For example, let's say that I want to solve the problem mentioned above with Api versioning.

I have the following controller:

[GenerateClient]
[ApiController]
[Route("api/v{version:apiVersion}/StringList")]
[ApiVersion("3.0")]
public class StringListController : Controller
{
    [HttpGet("Test")]
    public string Get()
    {
        return "test";
    }
}

and startup configuration

services.AddApiVersioning(o =>
        {
            o.AssumeDefaultVersionWhenUnspecified = true;
            o.DefaultApiVersion = new Microsoft.AspNetCore.Mvc.ApiVersion(1, 0);
            o.ReportApiVersions = true;
            o.ApiVersionReader = ApiVersionReader.Combine(
                new QueryStringApiVersionReader("api-version"),
                new HeaderApiVersionReader("X-Version"),
                new MediaTypeApiVersionReader("ver"));
        });

(both of these are taken from https://code-maze.com/aspnetcore-api-versioning/)

Now I can write the following test:

[Test]
public async Task TestWithApiVersioning()
{
    using var webApplicationFactory = 
        new WebApplicationFactory<Program>()
           .WithRidge(x =>
            {
                x.UseNunitLogWriter();
                x.HttpRequestFactoryMiddlewares.Add(new ApiVersionMiddleware());
            });

    var client = webApplicationFactory.CreateClient();
    var stringListControllerClient = new StringListControllerClient(client, webApplicationFactory.Services); 

    var response = await stringListControllerClient.Get();

    Assert.True(response.IsSuccessStatusCode);
    Assert.AreEqual("test", response.Result);
}

// We need to add custom middleware which sets "API version"
public class ApiVersionMiddleware : HttpRequestFactoryMiddleware
{
    public override async Task<HttpRequestMessage> CreateHttpRequest(
        IRequestFactoryContext requestFactoryContext)
    {
        requestFactoryContext.UrlGenerationParameters["version"] = "3";
        return await base.CreateHttpRequest(requestFactoryContext);
    }
}

And here is a log generated by Ridge that shows the generated request:

Request: Time when request was sent: 17:16:28:814 Method: GET, RequestUri: '/api/v3/StringList/Test', Version: 1.1, Content: , Headers: { ridgeCallId: 3172d341-c4b8-499c-9d8b-ea0a3ba16ba4 } Body:

Code Maze
API Versioning in ASP.NET Core - Code Maze
We’re going to talk about versioning a Rest API and explore the different versioning schemes we have in Asp.net Core.