【c#版本Openfeign】Net8 自帶OpenFeign實現遠端介面呼叫

四處觀察發表於2023-09-23

引言

    相信巨硬,我們便一直硬。Net版本到現在已經出了7了,8也已經在預覽版了,相信在一個半月就會正式釋出,其中也有很多拭目以待的新功能了,不僅僅有Apm和Tap的結合,TaskToAscynResult,以及UnsafeAccessor用來獲取私有變數,效能比反射,EMIT更高,還有針對AsyncLocal封裝的IAsyncContext,IAsyncState,用來存非同步上下文的一些資料,當然了,最讓我期待的還是自帶了一個OpenFeign,在看新增的東西的時候,其他的都覺得一般般,個人覺得哈,當看到這個AutoClient新增的包的時候,好奇心的驅使下,我點進去看了一下,哇,官網終於出這玩意了,使用簡單,根據特性,然後使用Sg來生成我們對應的實現從而我們只需要定義一個介面,打上特性,就可以生成一個對應的代理類,呼叫遠端Api介面,太令人心動,為此特地升級了VS,下載了Net8,體驗新功能,接下來,我們就看看他的使用案例。附官網連結:https://learn.microsoft.com/zh-cn/dotnet/api/microsoft.extensions.http.autoclient.autoclientattribute?view=dotnet-plat-ext-8.0

AutoClient

    在使用自帶的OpenFeign的時候,我們還需要下載一個擴充套件包 Microsoft.Extensions.Http.AutoClient,當然還有 Microsoft.Extensions.Http的擴充套件包了,接下來我們定義一個介面,IBussiness,打上AutoClient特性,第一個引數是我們在注入Httpclient的時候,給的名字,我這裡叫TestApi,這裡會根據使用了AutoClient特性自定生成一個BussIness的類,在下圖可以看到,自動生成了一個AutoClient.g.cs檔案,裡面的類就是Bussiness,其中包括了我們的TestPost方法以及路由資訊,在上面的程式碼中,我們使用了Post特性,代表我們這個是Post請求,以及方法引數限制必須有一個CancellationToken,這個Post裡面的內容,就是我另外一個專案種的介面地址。

  

builder.Services.AddHttpClient("TestApi",s=>s.BaseAddress=new Uri(" http://localhost:5062")); 
[AutoClient("TestApi")] public interface IBussiness { [Post("/Test/TestPost")] public Task<string> TestPost(CancellationToken cancellationToken); }

 

// <auto-generated/>
#nullable enable
#pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103

namespace WebApplication1.Api
{
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.AutoClient", "8.0.0.0")]
    public class Bussiness  : IBussiness
    {
        private static class Statics
        {
            public static readonly global::System.Net.Http.Headers.MediaTypeHeaderValue ApplicationJsonHeader = new("application/json")
            {
                CharSet = global::System.Text.Encoding.UTF8.WebName
            };

            public static readonly global::System.Net.Http.Headers.MediaTypeHeaderValue TextPlainHeader = new("text/plain")
            {
                CharSet = global::System.Text.Encoding.UTF8.WebName
            };

            public static readonly global::System.Uri UriTestPost = new("/Test/TestPost", global::System.UriKind.Relative);
            public static readonly global::Microsoft.Extensions.Http.Telemetry.RequestMetadata RequestMetadataTestPost = new()
            {
                DependencyName = "Bussiness",
                RequestName = "TestPost",
                RequestRoute = "/Test/TestPost"
            };

        }
        private readonly global::System.Net.Http.HttpClient _httpClient;
        private readonly global::Microsoft.Extensions.Http.AutoClient.AutoClientOptions _autoClientOptions;

        public Bussiness(global::System.Net.Http.HttpClient httpClient, global::Microsoft.Extensions.Http.AutoClient.AutoClientOptions autoClientOptions)
        {
            _httpClient = httpClient;
            _autoClientOptions = autoClientOptions;
        }

        public async global::System.Threading.Tasks.Task<string> TestPost(global::System.Threading.CancellationToken cancellationToken)
        {
            var httpRequestMessage = new global::System.Net.Http.HttpRequestMessage()
            {
                Method = global::System.Net.Http.HttpMethod.Post,
                RequestUri = Statics.UriTestPost,
            };

            try
            {
                global::Microsoft.Extensions.Telemetry.TelemetryExtensions.SetRequestMetadata(httpRequestMessage, Statics.RequestMetadataTestPost);

                return await SendRequest<string>("Bussiness", Statics.RequestMetadataTestPost.RequestRoute, httpRequestMessage, cancellationToken)
                    .ConfigureAwait(false);
            }
            finally
            {
                httpRequestMessage.Dispose();
            }
        }

        private async global::System.Threading.Tasks.Task<TResponse> SendRequest<TResponse>(
                    string dependencyName,
                    string path,
                    global::System.Net.Http.HttpRequestMessage httpRequestMessage,
                    global::System.Threading.CancellationToken cancellationToken)
            where TResponse : class
        {

            var response = await _httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);

            if (typeof(TResponse) == typeof(global::System.Net.Http.HttpResponseMessage))
            {
                return (response as TResponse)!;
            }

            try
            {
                if (!response.IsSuccessStatusCode)
                {
                    var error = await global::Microsoft.Extensions.Http.AutoClient.AutoClientHttpError.CreateAsync(response, cancellationToken).ConfigureAwait(false);
                    throw new global::Microsoft.Extensions.Http.AutoClient.AutoClientException(global::System.FormattableString.Invariant($"The '{dependencyName}' HTTP client failed with '{response.StatusCode}' status code."), path, error);
                }

                if (typeof(TResponse) == typeof(string))
                {
#if NET5_0_OR_GREATER
                    var rawContent = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
#else
                    cancellationToken.ThrowIfCancellationRequested();
                    var rawContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
#endif

                    return (rawContent as TResponse)!;
                }

                var mediaType = response.Content.Headers.ContentType?.MediaType;
                if (mediaType == "application/json")
                {
                    var deserializedResponse = await global::System.Net.Http.Json.HttpContentJsonExtensions.ReadFromJsonAsync<TResponse>(response.Content, _autoClientOptions.JsonSerializerOptions, cancellationToken)
                    .ConfigureAwait(false);
                    if (deserializedResponse == null)
                    {
                        var error = await global::Microsoft.Extensions.Http.AutoClient.AutoClientHttpError.CreateAsync(response, cancellationToken).ConfigureAwait(false);
                        throw new global::Microsoft.Extensions.Http.AutoClient.AutoClientException(global::System.FormattableString.Invariant($"The '{dependencyName}' REST API failed to deserialize response."), path, error);
                    }

                    return deserializedResponse;
                }

                var err = await global::Microsoft.Extensions.Http.AutoClient.AutoClientHttpError.CreateAsync(response, cancellationToken).ConfigureAwait(false);
                throw new global::Microsoft.Extensions.Http.AutoClient.AutoClientException(global::System.FormattableString.Invariant($"The '{dependencyName}' REST API returned an unsupported content type ('{mediaType}')."), path, err);

            }
            finally
            {
                response.Dispose();
            }
        }
    }

    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Gen.AutoClient", "8.0.0.0")]
    public static class AutoClientsExtensions
    {
        public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddBussiness(this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services)
        {
            return services.AddBussiness(_ => { });
        }

        public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddBussiness(
                this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services,
                global::System.Action<global::Microsoft.Extensions.Http.AutoClient.AutoClientOptions> configureOptions)
        {
            global::Microsoft.Extensions.DependencyInjection.OptionsServiceCollectionExtensions.AddOptionsWithValidateOnStart<global::Microsoft.Extensions.Http.AutoClient.AutoClientOptions, global::Microsoft.Extensions.Http.AutoClient.AutoClientOptionsValidator>(services, "Bussiness").Configure(configureOptions);
            global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.TryAddSingleton<IBussiness>(services, provider =>
            {
                var httpClient = global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<global::System.Net.Http.IHttpClientFactory>(provider).CreateClient("TestApi");
                var autoClientOptions = global::Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService<global::Microsoft.Extensions.Options.IOptionsMonitor<global::Microsoft.Extensions.Http.AutoClient.AutoClientOptions>>(provider).Get("Bussiness");
                return new Bussiness(httpClient, autoClientOptions);
            });
            return services;
        }

    }
}

 

  下面這段程式碼,是我另一個專案介面的程式碼,可以看到,路由是Test,方法的路由是TestPost,返回了一個字串true,因為,在使用AutoClient的時候,返回型別必須是引用型別,接下來,我們呼叫一下測試看看,在返回的結果中,我們可以看到返回了我們在另一個專案中返回的結果,true,同時,AutoClient還支援Get,Patch,Delete,Get,Put,Body(標記是在Body中),Header,Query等諸多特性,就是一個c#版本的OpenFeign,簡直爽的不要不要的。

[Route("Test")]
public class TestController : ControllerBase
{
    public TestController()
    {
            
    }
    [HttpPost("TestPost")]
    public Task<string> TestPost()
    { 
      return Task.FromResult("true");
    }
}

 

 結語

    今天就要開始十月一假期了,後續節後來了,會持續帶來新增api的一些玩法,包括IAsyncContext,還有其他的在等待探索,歡迎大家關注。

相關文章