diff --git a/src/Caller/Masa.Utils.Caller.Core/ICallerProvider.cs b/src/Caller/Masa.Utils.Caller.Core/ICallerProvider.cs index 87c6818..a77a3df 100644 --- a/src/Caller/Masa.Utils.Caller.Core/ICallerProvider.cs +++ b/src/Caller/Masa.Utils.Caller.Core/ICallerProvider.cs @@ -62,7 +62,8 @@ Task GetStringAsync( CancellationToken cancellationToken = default); Task GetStringAsync( - string? methodName, TRequest data, + string? methodName, + TRequest data, bool autoThrowUserFriendlyException = true, CancellationToken cancellationToken = default) where TRequest : class; diff --git a/src/Caller/Masa.Utils.Caller.Core/README.md b/src/Caller/Masa.Utils.Caller.Core/README.md new file mode 100644 index 0000000..3b35bcd --- /dev/null +++ b/src/Caller/Masa.Utils.Caller.Core/README.md @@ -0,0 +1,36 @@ +[中](README.zh-CN.md) | EN + +## Masa.Utils.Caller.Core + +Masa.Utils.Caller.Core is the basic class library of Caller, which provides the abstraction of the following capabilities + +* `ICallerFactory`: Factory for creating `CallerProvider` (Singleton) +* `ICallerProvider`: Provides `Post`, `Delete`, `Patch`, `Put`, `Get`, `Send` capabilities (Scoped) +* `IRequestMessage`: Provides the ability to process request data (default implementation [`JsonRequestMessage`](./JsonRequestMessage.cs)) (Singleton) +* `IResponseMessage`: Provides the ability to handle response data (default implementation [`DefaultResponseMessage`](./DefaultResponseMessage.cs)) (Singleton) +* `ITypeConvertProvider`: Provides the ability to convert types, support for `Get` requests of `ICallerProvider` (Singleton) + +## Summarize + +`Masa.Utils.Caller.Core` is the basic class library of Caller, but it cannot be used alone. Currently, Caller supports two implementations: + +* Implementation based on HttpClient: [Masa.Utils.Caller.HttpClient](../Masa.Utils.Caller.HttpClient/README.md) +* Implementation based on DaprClient: [Masa.Utils.Caller.DaprClient](../Masa.Utils.Caller.DaprClient/README.md) + +> Q: What should I do if the callee uses xml instead of json? +> +> A: Rewrite IRequestMessage and add the custom RequestMessage to the IServiceCollection before calling AddCaller + + ```` C# + services.AddSingleton(); + services.AddCaller(); + ```` + +> Q: If you want to handle custom StatusCode and throw exception information +> +> A: Rewrite IResponseMessage, add custom ResponseMessage to IServiceCollection before calling AddCaller + + ```` C# + services.AddSingleton(); + services.AddCaller(); + ```` \ No newline at end of file diff --git a/src/Caller/Masa.Utils.Caller.Core/README.zh-CN.md b/src/Caller/Masa.Utils.Caller.Core/README.zh-CN.md new file mode 100644 index 0000000..e4148a1 --- /dev/null +++ b/src/Caller/Masa.Utils.Caller.Core/README.zh-CN.md @@ -0,0 +1,36 @@ +中 | [EN](README.md) + +## Masa.Utils.Caller.Core + +Masa.Utils.Caller.Core是Caller的基础类库,提供了以下能力的抽象 + +* `ICallerFactory`: 工厂,用于创建`CallerProvider` (Singleton) +* `ICallerProvider`: 提供`Post`、`Delete`、`Patch`、`Put`、`Get`、`Send`的能力 (Scoped) +* `IRequestMessage`: 提供对请求数据处理的能力 (默认实现[`JsonRequestMessage`](./JsonRequestMessage.cs)) (Singleton) +* `IResponseMessage`: 提供对响应数据处理的能力 (默认实现[`DefaultResponseMessage`](./DefaultResponseMessage.cs)) (Singleton) +* `ITypeConvertProvider`: 提供类型转换的能力,为`ICallerProvider`的`Get`请求支撑 (Singleton) + +## 总结 + +`Masa.Utils.Caller.Core`是Caller的基础类库,但不能单独使用,目前Caller支持了两种实现方式: + +* 基于HttpClient的实现: [Masa.Utils.Caller.HttpClient](../Masa.Utils.Caller.HttpClient/README.zh-CN.md) +* 基于DaprClient的实现: [Masa.Utils.Caller.DaprClient](../Masa.Utils.Caller.DaprClient/README.zh-CN.md) + +> Q: 如果被调用方使用的是数据格式为xml,而不是json,我应该怎么做? +> +> A: 重写IRequestMessage,在调用AddCaller之前先将自定义的RequestMessage添加到IServiceCollection中 + + ``` C# + services.AddSingleton(); + services.AddCaller(); + ``` + +> Q: 如果希望处理自定义的StatusCode,并抛出异常信息 +> +> A: 重写IResponseMessage,在调用AddCaller之前先将自定义的ResponseMessage添加到IServiceCollection中 + + ``` C# + services.AddSingleton(); + services.AddCaller(); + ``` \ No newline at end of file diff --git a/src/Caller/Masa.Utils.Caller.Core/ServiceCollectionExtensions.cs b/src/Caller/Masa.Utils.Caller.Core/ServiceCollectionExtensions.cs index cadee56..6abdc94 100644 --- a/src/Caller/Masa.Utils.Caller.Core/ServiceCollectionExtensions.cs +++ b/src/Caller/Masa.Utils.Caller.Core/ServiceCollectionExtensions.cs @@ -25,44 +25,36 @@ public static IServiceCollection AddCaller(this IServiceCollection services, Act CallerOptions callerOption = new CallerOptions(services); options.Invoke(callerOption); - services.TryAddSingleton(); - services.TryAdd(new ServiceDescriptor(typeof(IRequestMessage), _ => new JsonRequestMessage(callerOption.JsonSerializerOptions), callerOption.CallerLifetime)); - services.TryAddSingleton(); + services.TryAddSingleton(serviceProvider => new DefaultCallerFactory(serviceProvider, callerOption)); + services.TryAddSingleton(_ => new JsonRequestMessage(callerOption.JsonSerializerOptions)); + services.TryAddSingleton(serviceProvider + => new DefaultResponseMessage(callerOption, serviceProvider.GetService>())); services.TryAddScoped(serviceProvider => serviceProvider.GetRequiredService().CreateClient()); services.TryAddSingleton(); services.AddAutomaticCaller(callerOption); - services.TryOrUpdateCallerOptions(callerOption); + CheckCallerOptions(callerOption); return services; } - private static IServiceCollection TryOrUpdateCallerOptions(this IServiceCollection services, CallerOptions options) + private static void CheckCallerOptions(CallerOptions options) { - services.TryAddSingleton(new CallerOptions(options.Services)); - var serviceProvider = services.BuildServiceProvider(); - var callerOptions = serviceProvider.GetRequiredService(); - - options.Callers.ForEach(caller => + if (options.Callers.GroupBy(r => r.Name).Any(x => x.Count() > 1)) { - if (callerOptions.Callers.Any(relation => relation.Name == caller.Name)) - throw new ArgumentException( - $"The caller name already exists, please change the name, the repeat name is [{caller.Name}]"); - - if (callerOptions.Callers.Any(relation => relation.IsDefault && caller.IsDefault)) - { - string errorCallerNames = string.Join("、", callerOptions.Callers - .Where(relation => relation.IsDefault) - .Select(relation => relation.Name) - .Concat(options.Callers.Where(relation => relation.IsDefault).Select(relation => relation.Name)) - .Distinct()); - throw new ArgumentException( - $"There can only be at most one default Caller Provider, and now the following Caller Providers are found to be default: {errorCallerNames}"); - } + var callerName = options.Callers.GroupBy(r => r.Name).Where(x => x.Count() > 1).Select(r => r.Key).FirstOrDefault(); + throw new ArgumentException($"The caller name already exists, please change the name, the repeat name is [{callerName}]"); + } - callerOptions.Callers.Add(caller); - }); - - return services; + if (options.Callers.Where(r => r.IsDefault).GroupBy(r => r.IsDefault).Any(x => x.Count() > 1)) + { + string errorCallerNames = string.Join("、", options.Callers + .Where(relation => relation.IsDefault) + .Select(relation => relation.Name) + .Concat(options.Callers.Where(relation => relation.IsDefault).Select(relation => relation.Name)) + .Distinct()); + throw new ArgumentException( + $"There can only be at most one default Caller Provider, and now the following Caller Providers are found to be default: {errorCallerNames}"); + } } private static void AddAutomaticCaller(this IServiceCollection services, CallerOptions callerOptions) diff --git a/src/Caller/Masa.Utils.Caller.DaprClient/README.md b/src/Caller/Masa.Utils.Caller.DaprClient/README.md new file mode 100644 index 0000000..66dd15d --- /dev/null +++ b/src/Caller/Masa.Utils.Caller.DaprClient/README.md @@ -0,0 +1,101 @@ +[中](README.zh-CN.md) | EN + +## Masa.Utils.Caller.DaprClient + +## Example: + +````c# +Install-Package Masa.Utils.Caller.DaprClient +```` + +### Basic usage: + +1. Modify `Program.cs` + + ```` C# + builder.Services.AddCaller(options => + { + options.UseDapr(clientBuilder => + { + clientBuilder.Name = "UserCaller";// The alias of the current Caller, when there is only one Provider, you can not assign a value to Name + clientBuilder.AppId = "" ;//AppID of the callee dapr + }); + }); + ```` + +2. How to use: + + ```` C# + app.MapGet("/Test/User/Hello", ([FromServices] ICallerProvider userCallerProvider, string name) + => userCallerProvider.GetAsync($"/Hello", new { Name = name })); + ```` + + > The interface address of the complete request is: http://localhost:3500/v1.0/invoke//method/Hello?Name={name} + +3. When there are multiple DaprClients, modify `Program.cs` + + ```` C# + builder.Services.AddCaller(options => + { + options.UseDapr(clientBuilder => + { + clientBuilder.Name = "UserCaller"; + clientBuilder.AppId = "" ;//AppID of the callee User service Dapr + }); + options.UseDapr(clientBuilder => + { + clientBuilder.Name = "OrderCaller"; + clientBuilder.AppId = "" ;//AppID of the callee Order service Dapr + }); + }); + ```` + +4. How to use UserCaller or OrderCaller + + ```` C# + app.MapGet("/Test/User/Hello", ([FromServices] ICallerProvider userCallerProvider, string name) + => userCallerProvider.GetAsync($"/Hello", new { Name = name })); + + + app.MapGet("/Test/Order/Hello", ([FromServices] ICallerFactory callerFactory, string name) => + { + var callerProvider = callerFactory.CreateClient("OrderCaller"); + return callerProvider.GetAsync($"/Hello", new { Name = name }); + }); + ```` + +> When multiple Callers are added, how to get the specified Caller? +>> Get the CallerProvider of the specified alias through the `CreateClient` method of `CallerFactory` +> +> Why doesn't `userCallerProvider` get the corresponding Caller through the `CreateClient` method of `CallerFactory`? +>> If no default ICallerProvider is specified, the default CallerProvider is the first one added in the `AddCaller` method + +#### Recommended usage + +1. Modify `Program.cs` + + ```` C# + builder.Services.AddCaller(); + ```` + +2. Add a new class `UserCaller` + + ```` C# + public class UserCaller: DaprCallerBase + { + protected override string AppId { get; set; } = ""; + + public HttpCaller(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + public Task HelloAsync(string name) => CallerProvider.GetStringAsync($"/Hello", new { Name = name }); + } + ```` + +3. How to use UserCaller + + ```` C# + app.MapGet("/Test/User/Hello", ([FromServices] UserCaller caller, string name) + => caller.HelloAsync(name)); + ```` \ No newline at end of file diff --git a/src/Caller/Masa.Utils.Caller.DaprClient/README.zh-CN.md b/src/Caller/Masa.Utils.Caller.DaprClient/README.zh-CN.md new file mode 100644 index 0000000..6e0856e --- /dev/null +++ b/src/Caller/Masa.Utils.Caller.DaprClient/README.zh-CN.md @@ -0,0 +1,101 @@ +中 | [EN](README.md) + +## Masa.Utils.Caller.DaprClient + +## 用例: + +```c# +Install-Package Masa.Utils.Caller.DaprClient +``` + +### 基本用法: + +1. 修改`Program.cs` + + ``` C# + builder.Services.AddCaller(options => + { + options.UseDapr(clientBuilder => + { + clientBuilder.Name = "UserCaller";// 当前Caller的别名,仅存在一个Provider时,可以不对Name赋值 + clientBuilder.AppId = "" ;//被调用方dapr的AppID + }); + }); + ``` + +2. 如何使用: + + ``` C# + app.MapGet("/Test/User/Hello", ([FromServices] ICallerProvider userCallerProvider, string name) + => userCallerProvider.GetAsync($"/Hello", new { Name = name })); + ``` + + > 完整请求的接口地址是:http://localhost:3500/v1.0/invoke//method/Hello?Name={name} + +3. 当存在多个DaprClient时,则修改`Program.cs`为 + + ``` C# + builder.Services.AddCaller(options => + { + options.UseDapr(clientBuilder => + { + clientBuilder.Name = "UserCaller"; + clientBuilder.AppId = "" ;//被调用方User服务Dapr的AppID + }); + options.UseDapr(clientBuilder => + { + clientBuilder.Name = "OrderCaller"; + clientBuilder.AppId = "" ;//被调用方Order服务Dapr的AppID + }); + }); + ``` + +4. 如何使用UserCaller或OrderCaller + + ``` C# + app.MapGet("/Test/User/Hello", ([FromServices] ICallerProvider userCallerProvider, string name) + => userCallerProvider.GetAsync($"/Hello", new { Name = name })); + + + app.MapGet("/Test/Order/Hello", ([FromServices] ICallerFactory callerFactory, string name) => + { + var callerProvider = callerFactory.CreateClient("OrderCaller"); + return callerProvider.GetAsync($"/Hello", new { Name = name }); + }); + ``` + +> 当多个Caller被添加时,如何获取指定的Caller? +>> 通过`CallerFactory`的`CreateClient`方法得到指定别名的CallerProvider +> +> 为什么`userCallerProvider`没有通过`CallerFactory`的`CreateClient`方法得到对应的Caller? +>> 如果未指定默认的ICallerProvider,则在`AddCaller`方法中第一个被添加的就是默认的CallerProvider + +#### 推荐用法 + +1. 修改`Program.cs` + + ``` C# + builder.Services.AddCaller(); + ``` + +2. 新增加类`UserCaller` + + ``` C# + public class UserCaller: DaprCallerBase + { + protected override string AppId { get; set; } = ""; + + public HttpCaller(IServiceProvider serviceProvider) : base(serviceProvider) + { + } + + public Task HelloAsync(string name) => CallerProvider.GetStringAsync($"/Hello", new { Name = name }); + } + ``` + +3. 如何使用UserCaller + + ``` C# + app.MapGet("/Test/User/Hello", ([FromServices] UserCaller caller, string name) + => caller.HelloAsync(name)); + ``` \ No newline at end of file diff --git a/src/Caller/Masa.Utils.Caller.HttpClient/README.md b/src/Caller/Masa.Utils.Caller.HttpClient/README.md index 992a58b..871c7a7 100644 --- a/src/Caller/Masa.Utils.Caller.HttpClient/README.md +++ b/src/Caller/Masa.Utils.Caller.HttpClient/README.md @@ -27,12 +27,12 @@ Install-Package Masa.Utils.Caller.HttpClient ```` C# app.MapGet("/Test/User/Hello", ([FromServices] ICallerProvider userCallerProvider, string name) - => userCallerProvider.GetAsync($"/Hello/{name}")); + => userCallerProvider.GetAsync($"/Hello", new { Name = name })); ```` - > The interface address of the complete request is: http://localhost:5000/Check/Healthy + > The interface address of the complete request is: http://localhost:5000/Hello?Name={name} -3. When there are multiple HttpClients, modify `Program.cs` as +3. When there are multiple HttpClients, modify `Program.cs` ```` C# builder.Services.AddCaller(options => @@ -54,13 +54,13 @@ Install-Package Masa.Utils.Caller.HttpClient ```` C# app.MapGet("/Test/User/Hello", ([FromServices] ICallerProvider userCallerProvider, string name) - => userCallerProvider.GetAsync($"/Hello/{name}")); + => userCallerProvider.GetAsync($"/Hello", new { Name = name })); app.MapGet("/Test/Order/Hello", ([FromServices] ICallerFactory callerFactory, string name) => { var callerProvider = callerFactory.CreateClient("OrderCaller"); - return callerProvider.GetAsync($"/Hello/{name}"); + return callerProvider.GetAsync($"/Hello", new { Name = name }); }); ```` @@ -89,7 +89,7 @@ Install-Package Masa.Utils.Caller.HttpClient { } - public Task HelloAsync(string name) => CallerProvider.GetStringAsync($"/Hello/{name}"); + public Task HelloAsync(string name) => CallerProvider.GetStringAsync($"/Hello", new { Name = name }); /// /// There is no need to overload by default, and it can be overloaded when there are special requirements for httpClient diff --git a/src/Caller/Masa.Utils.Caller.HttpClient/README.zh-CN.md b/src/Caller/Masa.Utils.Caller.HttpClient/README.zh-CN.md index 62aa028..37baf44 100644 --- a/src/Caller/Masa.Utils.Caller.HttpClient/README.zh-CN.md +++ b/src/Caller/Masa.Utils.Caller.HttpClient/README.zh-CN.md @@ -27,10 +27,10 @@ Install-Package Masa.Utils.Caller.HttpClient ``` C# app.MapGet("/Test/User/Hello", ([FromServices] ICallerProvider userCallerProvider, string name) - => userCallerProvider.GetAsync($"/Hello/{name}")); + => userCallerProvider.GetAsync($"/Hello", new { Name = name })); ``` - > 完整请求的接口地址是:http://localhost:5000/Check/Healthy + > 完整请求的接口地址是:http://localhost:5000/Hello?Name={name} 3. 当存在多个HttpClient时,则修改`Program.cs`为 @@ -54,13 +54,13 @@ Install-Package Masa.Utils.Caller.HttpClient ``` C# app.MapGet("/Test/User/Hello", ([FromServices] ICallerProvider userCallerProvider, string name) - => userCallerProvider.GetAsync($"/Hello/{name}")); + => userCallerProvider.GetAsync($"/Hello", new { Name = name })); app.MapGet("/Test/Order/Hello", ([FromServices] ICallerFactory callerFactory, string name) => { var callerProvider = callerFactory.CreateClient("OrderCaller"); - return callerProvider.GetAsync($"/Hello/{name}"); + return callerProvider.GetAsync($"/Hello", new { Name = name }); }); ``` @@ -89,7 +89,7 @@ Install-Package Masa.Utils.Caller.HttpClient { } - public Task HelloAsync(string name) => CallerProvider.GetStringAsync($"/Hello/{name}"); + public Task HelloAsync(string name) => CallerProvider.GetStringAsync($"/Hello", new { Name = name }); /// /// 默认不需要重载,对httpClient有特殊需求时可重载 diff --git a/test/Masa.Utils.Caller.Tests/CustomHttpClientCallerProvider.cs b/test/Masa.Utils.Caller.Tests/CustomHttpClientCallerProvider.cs index 73c33eb..e46ce06 100644 --- a/test/Masa.Utils.Caller.Tests/CustomHttpClientCallerProvider.cs +++ b/test/Masa.Utils.Caller.Tests/CustomHttpClientCallerProvider.cs @@ -8,7 +8,6 @@ public class CustomHttpClientCallerProvider : HttpClientCallerProvider public CustomHttpClientCallerProvider(IServiceProvider serviceProvider, string name, string baseApi) : base(serviceProvider, name, baseApi) { - } public string GetResult(string? methodName) => base.GetRequestUri(methodName); diff --git a/test/Masa.Utils.Caller.Tests/DefaultXmlResponseMessage.cs b/test/Masa.Utils.Caller.Tests/DefaultXmlResponseMessage.cs new file mode 100644 index 0000000..c5d2553 --- /dev/null +++ b/test/Masa.Utils.Caller.Tests/DefaultXmlResponseMessage.cs @@ -0,0 +1,91 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the MIT License. See LICENSE.txt in the project root for license information. + +namespace Masa.Utils.Caller.Tests; + +public class DefaultXmlResponseMessage : IResponseMessage +{ + private readonly ILogger? _logger; + + public DefaultXmlResponseMessage(ILogger? logger = null) + { + _logger = logger; + } + + public async Task ProcessResponseAsync(HttpResponseMessage response, + CancellationToken cancellationToken = default) + { + if (response.IsSuccessStatusCode) + { + switch (response.StatusCode) + { + case HttpStatusCode.Accepted: + case HttpStatusCode.NoContent: + return default; + case (HttpStatusCode)MasaHttpStatusCode.UserFriendlyException: + throw new UserFriendlyException(await response.Content.ReadAsStringAsync(cancellationToken)); + default: + if (typeof(TResponse) == typeof(Guid) || typeof(TResponse) == typeof(Guid?)) + { + var content = await response.Content.ReadAsStringAsync(cancellationToken); + if (string.IsNullOrEmpty(content)) + return (TResponse)(object?)null!; + + return (TResponse?)(object)Guid.Parse(content.Replace("\"", "")); + } + if (typeof(TResponse) == typeof(DateTime) || typeof(TResponse) == typeof(DateTime?)) + { + var content = await response.Content.ReadAsStringAsync(cancellationToken); + if (string.IsNullOrEmpty(content)) + return (TResponse)(object?)null!; + + return (TResponse?)(object)DateTime.Parse(content.Replace("\"", "")); + } + if (typeof(TResponse).GetInterfaces().Any(type => type == typeof(IConvertible))) + { + var content = await response.Content.ReadAsStringAsync(cancellationToken); + return (TResponse)Convert.ChangeType(content, typeof(TResponse)); + } + try + { + var res = await response.Content.ReadAsStringAsync(cancellationToken); + return XmlUtils.Deserialize(res) ?? + throw new ArgumentException("The response cannot be empty or there is an error in deserialization"); + } + catch (Exception exception) + { + _logger?.LogWarning(exception, exception.Message); + ExceptionDispatchInfo.Capture(exception).Throw(); + return default; //This will never be executed, the previous line has already thrown an exception + } + } + } + + await ProcessResponseExceptionAsync(response, cancellationToken); + return default; //never executed + } + + public async Task ProcessResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) + { + if (response.IsSuccessStatusCode) + { + switch (response.StatusCode) + { + case (HttpStatusCode)MasaHttpStatusCode.UserFriendlyException: + throw new UserFriendlyException(await response.Content.ReadAsStringAsync(cancellationToken)); + default: + return; + } + } + + await ProcessResponseExceptionAsync(response, cancellationToken); + } + + public async Task ProcessResponseExceptionAsync(HttpResponseMessage response, CancellationToken cancellationToken = default) + { + if (response.Content.Headers.ContentLength is > 0) + throw new Exception(await response.Content.ReadAsStringAsync(cancellationToken)); + + throw new MasaException($"ReasonPhrase: {response.ReasonPhrase ?? string.Empty}, StatusCode: {response.StatusCode}"); + } +} diff --git a/test/Masa.Utils.Caller.Tests/HttpClientCallerTest.cs b/test/Masa.Utils.Caller.Tests/HttpClientCallerTest.cs index 0066750..eba6de0 100644 --- a/test/Masa.Utils.Caller.Tests/HttpClientCallerTest.cs +++ b/test/Masa.Utils.Caller.Tests/HttpClientCallerTest.cs @@ -1,6 +1,8 @@ -// Copyright (c) MASA Stack All rights reserved. +// Copyright (c) MASA Stack All rights reserved. // Licensed under the MIT License. See LICENSE.txt in the project root for license information. +using System.Net.Http.Json; + namespace Masa.Utils.Caller.Tests; [TestClass] @@ -34,4 +36,95 @@ public void TestGetRequestUri(string prefix, string methods, string result) var provider = new CustomHttpClientCallerProvider(serviceProvider, string.Empty, prefix); Assert.IsTrue(provider.GetResult(methods) == result); } + + [TestMethod] + public async Task TestRequestDataIsXmlAsync() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + Mock httpClientFactory = new(); + var handlerMock = new Mock(); + var magicHttpClient = new System.Net.Http.HttpClient(handlerMock.Object) + { + BaseAddress = new Uri("http://localhost:5000") + }; + var response = new BaseResponse("success"); + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(XmlUtils.Serializer(response)) + }) + .Verifiable(); + + httpClientFactory.Setup(factory => factory.CreateClient(It.IsAny())).Returns(magicHttpClient); + services.AddSingleton(httpClientFactory.Object); + var serviceProvider = services.BuildServiceProvider(); + string name = ""; + string prefix = ""; + var httpClientCallerProvider = new HttpClientCallerProvider(serviceProvider, name, prefix); + + var res = await httpClientCallerProvider.PostAsync("Hello", new RegisterUser("Jim", "123456")); + Assert.IsNotNull(res); + Assert.IsTrue(res.Code == response.Code); + } + + [TestMethod] + public async Task TestRequestMessageReturnOnceAsync() + { + var services = new ServiceCollection(); + RegisterUser registerUser = new RegisterUser("Jim", "123456"); + + services.AddSingleton(); + Mock requestMessage = new(); + requestMessage.Setup(req => req.ProcessHttpRequestMessageAsync(It.IsAny())) + .ReturnsAsync(new HttpRequestMessage(HttpMethod.Post, "Hello")).Verifiable(); + requestMessage.Setup(req => req.ProcessHttpRequestMessageAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new HttpRequestMessage(HttpMethod.Post, "Hello") + { + Content = JsonContent.Create(registerUser) + }).Verifiable(); + services.AddSingleton(_ => requestMessage.Object); + services.AddSingleton(); + Mock httpClientFactory = new(); + var handlerMock = new Mock(); + var magicHttpClient = new System.Net.Http.HttpClient(handlerMock.Object) + { + BaseAddress = new Uri("http://localhost:5000") + }; + var response = new BaseResponse("success"); + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.IsAny(), + ItExpr.IsAny() + ) + .ReturnsAsync(new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(XmlUtils.Serializer(response)) + }) + .Verifiable(); + + httpClientFactory.Setup(factory => factory.CreateClient(It.IsAny())).Returns(magicHttpClient); + services.AddSingleton(httpClientFactory.Object); + var serviceProvider = services.BuildServiceProvider(); + string name = ""; + string prefix = ""; + var httpClientCallerProvider = new HttpClientCallerProvider(serviceProvider, name, prefix); + + var res = await httpClientCallerProvider.PostAsync("Hello", registerUser); + Assert.IsNotNull(res); + Assert.IsTrue(res.Code == response.Code); + requestMessage.Verify(r => r.ProcessHttpRequestMessageAsync(It.IsAny(), It.IsAny()), Times.Once); + } } diff --git a/test/Masa.Utils.Caller.Tests/Requesties/RegisterUser.cs b/test/Masa.Utils.Caller.Tests/Requesties/RegisterUser.cs new file mode 100644 index 0000000..6752613 --- /dev/null +++ b/test/Masa.Utils.Caller.Tests/Requesties/RegisterUser.cs @@ -0,0 +1,22 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the MIT License. See LICENSE.txt in the project root for license information. + +namespace Masa.Utils.Caller.Tests.Requesties; + +[XmlRoot] +public class RegisterUser +{ + [XmlElement] + public string Account { get; set; } + + [XmlElement] + public string Password { get; set; } + + public RegisterUser() { } + + public RegisterUser(string account, string password) : this() + { + Account = account; + Password = password; + } +} diff --git a/test/Masa.Utils.Caller.Tests/Response/BaseResponse.cs b/test/Masa.Utils.Caller.Tests/Response/BaseResponse.cs new file mode 100644 index 0000000..fbbe5a1 --- /dev/null +++ b/test/Masa.Utils.Caller.Tests/Response/BaseResponse.cs @@ -0,0 +1,16 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the MIT License. See LICENSE.txt in the project root for license information. + +namespace Masa.Utils.Caller.Tests.Response; + +[Serializable] +[XmlRoot] +public class BaseResponse +{ + [XmlElement] + public string Code { get; set; } + + public BaseResponse() { } + + public BaseResponse(string code) { Code = code; } +} diff --git a/test/Masa.Utils.Caller.Tests/Utils/XmlUtils.cs b/test/Masa.Utils.Caller.Tests/Utils/XmlUtils.cs new file mode 100644 index 0000000..cf66362 --- /dev/null +++ b/test/Masa.Utils.Caller.Tests/Utils/XmlUtils.cs @@ -0,0 +1,23 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the MIT License. See LICENSE.txt in the project root for license information. + +namespace Masa.Utils.Caller.Tests.Utils; + +public class XmlUtils +{ + public static string Serializer(object data) + { + MemoryStream ms = new MemoryStream(); + StreamWriter sw = new StreamWriter(ms, Encoding.UTF8); + XmlSerializer xz = new XmlSerializer(data.GetType()); + xz.Serialize(sw, data); + return Encoding.UTF8.GetString(ms.ToArray()); + } + + public static T Deserialize(string xml) + { + XmlSerializer xmlSerializer = new XmlSerializer(typeof(T)); + using MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(xml.ToCharArray())); + return (T)xmlSerializer.Deserialize(stream)!; + } +} diff --git a/test/Masa.Utils.Caller.Tests/XmlRequestMessage.cs b/test/Masa.Utils.Caller.Tests/XmlRequestMessage.cs new file mode 100644 index 0000000..03cf570 --- /dev/null +++ b/test/Masa.Utils.Caller.Tests/XmlRequestMessage.cs @@ -0,0 +1,17 @@ +// Copyright (c) MASA Stack All rights reserved. +// Licensed under the MIT License. See LICENSE.txt in the project root for license information. + +namespace Masa.Utils.Caller.Tests; + +public class XmlRequestMessage : IRequestMessage +{ + public Task ProcessHttpRequestMessageAsync(HttpRequestMessage requestMessage) + => Task.FromResult(requestMessage); + + public Task ProcessHttpRequestMessageAsync(HttpRequestMessage requestMessage, TRequest data) + { + var xmlContent = XmlUtils.Serializer(data!); + requestMessage.Content = new StringContent(xmlContent); + return Task.FromResult(requestMessage); + } +} diff --git a/test/Masa.Utils.Caller.Tests/_Imports.cs b/test/Masa.Utils.Caller.Tests/_Imports.cs index 7f008ae..f7a1a4d 100644 --- a/test/Masa.Utils.Caller.Tests/_Imports.cs +++ b/test/Masa.Utils.Caller.Tests/_Imports.cs @@ -4,8 +4,18 @@ global using Masa.Utils.Caller.Core; global using Masa.Utils.Caller.HttpClient; global using Masa.Utils.Caller.Tests.Queries; +global using Masa.Utils.Caller.Tests.Requesties; +global using Masa.Utils.Caller.Tests.Response; +global using Masa.Utils.Caller.Tests.Utils; +global using Masa.Utils.Exceptions; global using Microsoft.AspNetCore.Builder; global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Logging; global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Moq; +global using Moq.Protected; global using System.Net; +global using System.Runtime.ExceptionServices; +global using System.Text; global using System.Text.Json.Serialization; +global using System.Xml.Serialization;