引言
相信巨硬,我們便一直硬。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,還有其他的在等待探索,歡迎大家關注。