dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
34.77k stars 9.82k forks source link

Generating REST Clients for .NET Minimal / Web API APIs. #36636

Open rafikiassumani-msft opened 2 years ago

rafikiassumani-msft commented 2 years ago

Is your feature request related to a problem? Please describe.

Investigate how retrofit or refit can be used for REST Client generations with dotnet-api tooling.

Describe the solution you'd like

Retrofit or refit are great libraries for generating REST clients for mostly android apps. They allow you to define an interface containing a set of endpoints that your application will be interacting with, and then you can generate the implementation code. We would like to investigate the possibility of using retrofit/refit/or something new for generating REST Client implementations with dotnet-api tooling.

Additional context

davidfowl commented 2 years ago

@bradygaster is interested in this as well

rafikiassumani-msft commented 2 years ago

@davidfowl I just assigned it to him. We had a conversation on this.

WhatzGames commented 1 year ago

Just putting down some thoughts that came to mind today.

Would't it be possible to use a Source generator for this case?

Using Attributes in a way of something like this:

//Example partially taken from reactiveui/refit
public interface IGithubApi
{
    [Get("/users")]
    [Response(ResponseType.Xml)] //Using XmlDeserializer on this endpoint
    public IAsyncEnumerable<User> GetUserAsync();

    [Get("/users")]
    [Response(ResponseType.Json)] //Using JsonDeserializer
    public IAsyncEnumerable<User> GetUsersAsync(CancellationToken cancellationToken);

    [Get("/users/{user}")]
    public Task<User> GetUserAsync(string user, CancellationToken cancellationToken);

    [Get("/users/{user}")]
    public Stream GetUserStreamAsync(string user, CancellationToken cancellationToken);
}

[Apiclient<IGithubApi>] //[ApiClient(typeof(IGithubClient))]
public partial class GithubApi
{

}

which would result in something like this:

public partial class GithubApi : IGithubApi
{
    private readonly HttpClient _client;

    public GithubApi(HttpClient client)
    {
        _client = client;
    }

    public async IAsyncEnumerable<User> GetUsersAsync()
    {
        using var responseMessage = await _client.GetAsync("/users");
        if (!responseMessage.IsSuccessStatusCode)
        {
            //something doing here?
        }
        await using var stream = await responseMessage.Content.ReadAsStreamAsync();
        var reader = XmlReader.Create(stream);
        var serializer = new XmlSerializer(typeof(User[]));
        if (!serializer.CanDeserialize(reader))
        {
            throw new InvalidOperationException("Not Deserializable");
        }

        var entries =  (User[])serializer.Deserialize(reader);
        if (entries == null) yield break;

        foreach (var entry in entries)
        {
            yield return entry;
        }
    }

    public async IAsyncEnumerable<User> GetUsersAsync([EnumeratorCancellation]CancellationToken cancellationToken)
    {
        using var responseMessage = await _client.GetAsync("/users", cancellationToken);
        if (!responseMessage.IsSuccessStatusCode)
        {
            //something doing here?
        }
        await using var stream = await responseMessage.Content.ReadAsStreamAsync(cancellationToken);
        var entries = JsonSerializer.DeserializeAsyncEnumerable<User>(stream, JsonSerializerOptions.Default, cancellationToken);
        await foreach (var entry in entries.WithCancellation(cancellationToken))
        {
            yield return entry;
        }
    }

    public Task<User> GetUserAsync(string user, CancellationToken cancellationToken)
    {

    }

    public async Task<Stream> GetUserStreamAsync(string user, CancellationToken cancellationToken)
    {
        using var responseMessage = await _client.GetAsync($"/users/{user}", cancellationToken);
        if (!responseMessage.IsSuccessStatusCode)
        {
            //something doing here?
        }
        return await responseMessage.Content.ReadAsStreamAsync(cancellationToken);
    }

    public async Task<string> GetUserStringAsync(string user, CancellationToken cancellationToken)
    {
        using var responseMessage = await _client.GetAsync($"/users/{user}", cancellationToken);
        if (!responseMessage.IsSuccessStatusCode)
        {
            //something doing here?
        }
        return stream = await responseMessage.Content.ReadAsStringAsync(cancellationToken);
    }
}

It's just something to show what I had in mind and maybe it's something worth thinking about?

I was also thinking of a way to proccess different StatusCodes, and thought of the TypedResults.NotFound, Ok etc.... When using Results<> With the corresponding Typed parameters as returnparameter, then based on that the StatusCode could be filtered out when processing the responsemessage and returned, but I haven't thought of it any further.

davidfowl commented 1 year ago

See https://github.com/reactiveui/refit

bradygaster commented 1 year ago

I love the idea of a source generator that outputs Refit clients. I spoke with @clairernovotny about this a few times. My goal would be to have a Refit provider for the REST API client generation feature in VS Connected Services. So, cc @vijayrkn, with whom I've discussed this idea a few times and seemed receptive to it.