├── .gitignorecd ├── 并行队列.png ├── package-icon.png ├── examples ├── Http │ ├── ApiServer │ │ ├── Startup.cs │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ ├── ApiServer.csproj │ │ ├── WeatherForecast.cs │ │ ├── Controllers │ │ │ ├── LimitController.cs │ │ │ ├── WeatherForecastController.cs │ │ │ └── TrackingController.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ └── BasicAuthenticationHandler.cs │ ├── BasicAuth │ │ ├── BasicAuth.csproj │ │ └── Program.cs │ ├── RequestLog │ │ ├── RequestLog.csproj │ │ ├── TestWebApplicationFactory.cs │ │ └── Program.cs │ ├── SDKExample │ │ ├── SDKExample.csproj │ │ ├── SDK │ │ │ ├── TestApiClient.cs │ │ │ └── SDKServiceCollectionExtensions.cs │ │ └── Program.cs │ └── HttpHeaderTracking │ │ ├── HttpHeaderTracking.csproj │ │ ├── ServiceBFactory.cs │ │ ├── ServiceAFactory.cs │ │ └── Program.cs ├── AutoMapper │ └── Example │ │ ├── TargetB.cs │ │ ├── SourceA.cs │ │ ├── TargetA.cs │ │ ├── Example.csproj │ │ └── Program.cs ├── ParallelQueue │ └── ParallelQueueExample │ │ ├── NotifyMessage.cs │ │ ├── ParallelQueueExample.csproj │ │ ├── Handlers │ │ ├── MessageHandlerA.cs │ │ └── MessageHandlerB.cs │ │ └── Program.cs ├── MockHttpRequest │ ├── MockHttpRequest.csproj │ └── Program.cs └── Logging │ ├── JsonFileLogging │ ├── JsonFileLogging.csproj │ └── Program.cs │ ├── JsonConsoleLogging │ ├── JsonConsoleLogging.csproj │ └── Program.cs │ ├── NormalFileLogging │ ├── NormalFileLogging.csproj │ └── Program.cs │ └── NormalConsoleLogging │ ├── NormalConsoleLogging.csproj │ └── Program.cs ├── src ├── Extensions │ ├── ParallelQueue │ │ ├── QueueHandlers │ │ │ ├── IQueueHandler.cs │ │ │ ├── QueueHandlerBase.cs │ │ │ ├── QueueProcessor.cs │ │ │ ├── QueueHandlerContext.cs │ │ │ └── QueueHandlerFactory.cs │ │ ├── IParallelQueueProducerFactory.cs │ │ ├── IParallelQueueProducer.cs │ │ ├── ParallelQueueProducerOptions.cs │ │ ├── Memory │ │ │ ├── MemoryOptionsExtensions.cs │ │ │ └── MemoryQueueProducer.cs │ │ ├── IParallelQueueConsumerFactory.cs │ │ ├── IParallelQueueConsumer.cs │ │ ├── FactoryExtensions.cs │ │ ├── QueueExecutorLoadInfo.cs │ │ ├── ParallelQueueConsumerOptions.cs │ │ ├── DefaultParallelQueueProducerFactory.cs │ │ ├── ParallelQueueServiceConnectionExtensions.cs │ │ ├── DefaultParallelQueueConsumerFactory.cs │ │ └── ParallelQueueHostedService.cs │ ├── TokenProvider │ │ ├── CurrentClientInfo.cs │ │ ├── IClientCertificateProvider.cs │ │ ├── ClientCertificateInfo.cs │ │ ├── ClientCertificateToken.cs │ │ ├── TokenCache.cs │ │ ├── CheckResponseProcessor.cs │ │ ├── CertificateProcessor.cs │ │ ├── SetTokenProcessor.cs │ │ ├── ClientCertificateProvider.cs │ │ └── TokenProviderServiceCollectionExtensions.cs │ ├── AutoMapper │ │ ├── MapperEnumNameAttribute.cs │ │ ├── MapperPropertyNameAttribute.cs │ │ ├── IMapperProvider.cs │ │ ├── DefaultMapperProvider.cs │ │ ├── IMapper.cs │ │ ├── MapperServiceCollectionExtensions.cs │ │ ├── IMapperProviderExtensions.cs │ │ ├── MapperOptions.cs │ │ └── CircularRefChecker.cs │ ├── Logger │ │ ├── ILoggerExtensions.cs │ │ ├── TestLoggerProviderLogBuilderExtensions.cs │ │ ├── LogPathAndTimeKey.cs │ │ ├── TestLogContent.cs │ │ ├── TestLoggerProviderServiceCollectionExtensions.cs │ │ ├── LogLevelConverter.cs │ │ ├── TestLoggerProvider.cs │ │ └── Json │ │ │ └── CompactJsonFormatter.cs │ ├── HttpServer │ │ └── EnableBufferingAttribute.cs │ ├── WebApiStartupFilter.cs │ ├── Utils │ │ ├── HttpRequestExtensions.cs │ │ ├── IAutoRetry.cs │ │ ├── ObjectExtensions.cs │ │ ├── StringExtensions.cs │ │ ├── DistributedCacheExtensions.cs │ │ └── JsonHelper.cs │ ├── HttpClient │ │ ├── DisableEnsureSuccessStatusCodeMessageHandler.cs │ │ ├── MessageHandlerServiceCollectionExtensions.cs │ │ ├── MockHttpMessageHandler.cs │ │ ├── GetClientTokenMessageHandler.cs │ │ ├── MessageHandlerFilter.cs │ │ ├── TransRequestHeadersMessageHandler.cs │ │ ├── HttpClientBuilderExtensions.cs │ │ ├── HttpMessageExtensions.cs │ │ ├── LoggingDetailMessageHandler.cs │ │ └── MockHttpMessageHandlerOptions.cs │ ├── LogPathTemplates.cs │ ├── Entities │ │ ├── PagingRequest.cs │ │ └── ResponseMessage.cs │ ├── Xfrogcn.AspNetCore.Extensions.sln │ ├── TrackingHeaders.cs │ ├── Xfrogcn.AspNetCore.Extensions.csproj │ ├── ServiceCollectionExtensions.cs │ ├── Expressions │ │ └── PredicateBuilder.cs │ ├── IHostBuilderExtensions.cs │ └── WebApiHostBuilderExtensions.cs └── Extensions.RedisQueueProducer │ ├── QueueProducerServiceCollectionExtensions.cs │ ├── RedisOptions.cs │ ├── OptionsExtensions.cs │ ├── Xfrogcn.AspNetCore.Extensions.RedisQueueProducer.csproj │ └── RedisConnectionManager.cs ├── test └── Extensions.Tests │ ├── Properties │ └── launchSettings.json │ ├── TokenProvider │ ├── MockTokenProcessor.cs │ └── CertificateProcessorTest.cs │ ├── Extensions.Tests.csproj │ ├── WebApiHostExtensionsTest.cs │ └── AutoMapper │ └── GenearatorTest.cs ├── doc ├── HttpServer.md ├── RequestTracing.md ├── HttpMessageExtensions.md ├── TokenRequest.md ├── MockHttpReuest.md ├── Logging.md ├── ParallelQueue.md ├── TokenProvider.md └── LightweightMapper.md ├── Directory.Build.props ├── LICENSE ├── .vscode ├── tasks.json └── launch.json └── README.md /.gitignorecd: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /并行队列.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xfrogcn/Xfrogcn.AspNetCore.Extensions/HEAD/并行队列.png -------------------------------------------------------------------------------- /package-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xfrogcn/Xfrogcn.AspNetCore.Extensions/HEAD/package-icon.png -------------------------------------------------------------------------------- /examples/Http/ApiServer/Startup.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xfrogcn/Xfrogcn.AspNetCore.Extensions/HEAD/examples/Http/ApiServer/Startup.cs -------------------------------------------------------------------------------- /examples/AutoMapper/Example/TargetB.cs: -------------------------------------------------------------------------------- 1 | namespace Example 2 | { 3 | class TargetB : TargetA 4 | { 5 | public string C { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/AutoMapper/Example/SourceA.cs: -------------------------------------------------------------------------------- 1 | namespace Example 2 | { 3 | class SourceA 4 | { 5 | public string A { get; set; } 6 | 7 | public int? B { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/AutoMapper/Example/TargetA.cs: -------------------------------------------------------------------------------- 1 | namespace Example 2 | { 3 | class TargetA 4 | { 5 | public string A { get; set; } 6 | 7 | public long B { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/Http/ApiServer/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/ParallelQueue/ParallelQueueExample/NotifyMessage.cs: -------------------------------------------------------------------------------- 1 | namespace ParallelQueueExample 2 | { 3 | public class NotifyMessage 4 | { 5 | public int Input { get; set; } 6 | 7 | public int Output { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/Http/ApiServer/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/QueueHandlers/IQueueHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Xfrogcn.AspNetCore.Extensions 4 | { 5 | public interface IQueueHandler 6 | { 7 | int Order { get; } 8 | 9 | Task Process(QueueHandlerContext context); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Extensions/TokenProvider/CurrentClientInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Xfrogcn.AspNetCore.Extensions 6 | { 7 | /// 8 | /// 默认的客户端认证信息 9 | /// 10 | public class CurrentClientInfo : ClientCertificateInfo 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Extensions/TokenProvider/IClientCertificateProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Xfrogcn.AspNetCore.Extensions 6 | { 7 | public interface IClientCertificateProvider 8 | { 9 | ClientCertificateManager GetClientCertificateManager(string clientId); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/Http/ApiServer/ApiServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/MockHttpRequest/MockHttpRequest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/Http/ApiServer/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ApiServer 4 | { 5 | public class WeatherForecast 6 | { 7 | public DateTime Date { get; set; } 8 | 9 | public int TemperatureC { get; set; } 10 | 11 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 12 | 13 | public string Summary { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Extensions/AutoMapper/MapperEnumNameAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Xfrogcn.AspNetCore.Extensions 3 | { 4 | [AttributeUsage(AttributeTargets.Field)] 5 | public class MapperEnumNameAttribute : Attribute 6 | { 7 | public MapperEnumNameAttribute(string name) 8 | { 9 | Name = name; 10 | } 11 | 12 | public string Name { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Extensions/TokenProvider/ClientCertificateInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Xfrogcn.AspNetCore.Extensions 3 | { 4 | public class ClientCertificateInfo 5 | { 6 | public string ClientID { get; set; } 7 | 8 | public string ClientName { get; set; } 9 | 10 | public string AuthUrl { get; set; } 11 | 12 | public string ClientSecret { get; set; } 13 | 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Extensions/TokenProvider/ClientCertificateToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Xfrogcn.AspNetCore.Extensions 3 | { 4 | /// 5 | /// 认证结果 6 | /// 7 | [Serializable] 8 | public class ClientCertificateToken 9 | { 10 | public string token_type { get; set; } 11 | 12 | public string access_token { get; set; } 13 | 14 | public long expires_in { get; set; } 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/Extensions.Tests/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true 5 | }, 6 | "profiles": { 7 | "Extensions.Tests": { 8 | "commandName": "Project", 9 | "launchBrowser": true, 10 | "applicationUrl": "http://localhost:53896", 11 | "environmentVariables": { 12 | "ASPNETCORE_ENVIRONMENT": "Development" 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /doc/HttpServer.md: -------------------------------------------------------------------------------- 1 | # Http服务端扩展 2 | 3 | ## 请求详细记录 4 | 5 | 通过将配置的ServerRequestLevel设置为Verbose,可记录每一个请求及应答的详细内容。 6 | 7 | ```c# 8 | builder.UseExtensions(null, config => 9 | { 10 | // 将服务端请求日志层级设置为Verbose可记录服务端请求详细日志 11 | config.ServerRequestLevel = Serilog.Events.LogEventLevel.Verbose; 12 | }); 13 | ``` 14 | 15 | ## 开启请求Buffer 16 | 17 | 默认情况下,AspNetCore中的请求内容只可读取一次,如果你需要多次读取,可通过`EnableBufferingAttribute`在Action或Controller上开启请求缓冲。 18 | -------------------------------------------------------------------------------- /doc/RequestTracing.md: -------------------------------------------------------------------------------- 1 | # 请求跟踪 2 | 3 | 扩展库可以配置HttpClient自动传递指定的Http请求头来辅助完成请求跟踪功能。 4 | 5 | 要通过HttpClient自动向下传递请求头,只需通过WebApiConfig的TrackingHeaders属性配置需要传递的请求头,默认配置为x-*,表示传递所有以x-为前缀的请求头。 6 | 7 | ```c# 8 | IServiceCollection sc = new ServiceCollection() 9 | .AddExtensions(null, config => 10 | { 11 | // 以下配置传递以my-为前缀的请求头 12 | config.TrackingHeaders.Add("my-*"); 13 | }); 14 | ``` 15 | 16 | 有关请求头传递的示例,请参考`examples/Http/HttpHeaderTracking` -------------------------------------------------------------------------------- /examples/Http/ApiServer/Controllers/LimitController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace ApiServer.Controllers 5 | { 6 | [ApiController] 7 | [Route("{controller}")] 8 | [Authorize(AuthenticationSchemes = "BasicAuthentication")] 9 | public class LimitController : ControllerBase 10 | { 11 | 12 | public string Test() 13 | { 14 | return User.Identity.Name; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/IParallelQueueProducerFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Xfrogcn.AspNetCore.Extensions 2 | { 3 | public interface IParallelQueueProducerFactory 4 | { 5 | /// 6 | /// 创建队列生产者 7 | /// 8 | /// 队列项类型 9 | /// 队列名称 10 | /// 11 | IParallelQueueProducer CreateProducer(string queueName); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/AutoMapper/Example/Example.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/Logging/JsonFileLogging/JsonFileLogging.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Extensions/AutoMapper/MapperPropertyNameAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Xfrogcn.AspNetCore.Extensions 6 | { 7 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] 8 | public class MapperPropertyNameAttribute : Attribute 9 | { 10 | public string Name { get; set; } 11 | 12 | public Type TargetType { get; set; } 13 | 14 | public Type SourceType { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/Logging/JsonConsoleLogging/JsonConsoleLogging.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/Logging/NormalFileLogging/NormalFileLogging.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/Logging/NormalConsoleLogging/NormalConsoleLogging.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/ParallelQueue/ParallelQueueExample/ParallelQueueExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Extensions/Logger/ILoggerExtensions.cs: -------------------------------------------------------------------------------- 1 | using Serilog.Events; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xfrogcn.AspNetCore.Extensions; 6 | 7 | namespace Microsoft.Extensions.Logging 8 | { 9 | public static class ILoggerExtensions 10 | { 11 | public static bool IsEnabled(this ILogger logger, LogEventLevel level) 12 | { 13 | LogLevel logLevel = LogLevelConverter.Converter(level); 14 | return logger.IsEnabled(logLevel); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Extensions/Logger/TestLoggerProviderLogBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Microsoft.Extensions.Logging 7 | { 8 | public static class TestLoggerProviderLogBuilderExtensions 9 | { 10 | public static ILoggingBuilder AddTestLogger(this ILoggingBuilder loggingBuilder) 11 | { 12 | loggingBuilder.Services.AddTestLoggerProvider(); 13 | return loggingBuilder; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Extensions/TokenProvider/TokenCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Xfrogcn.AspNetCore.Extensions 3 | { 4 | [Serializable] 5 | public class TokenCache : ClientCertificateToken 6 | { 7 | public DateTime LastGetTime { get; set; } 8 | 9 | public bool IsExpired() 10 | { 11 | if (expires_in <= 0 || ((DateTime.UtcNow - LastGetTime.ToUniversalTime()).TotalSeconds - expires_in) >= -30) 12 | { 13 | return true; 14 | } 15 | return false; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/Http/BasicAuth/BasicAuth.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/Http/RequestLog/RequestLog.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/Http/SDKExample/SDKExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/QueueHandlers/QueueHandlerBase.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Xfrogcn.AspNetCore.Extensions 4 | { 5 | public class QueueHandlerBase : IQueueHandler 6 | { 7 | public virtual int Order => 100; 8 | 9 | 10 | public virtual Task Process(QueueHandlerContext context) 11 | { 12 | return Task.CompletedTask; 13 | } 14 | } 15 | 16 | public class QueueHandlerBase : QueueHandlerBase 17 | { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/Http/HttpHeaderTracking/HttpHeaderTracking.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/Http/SDKExample/SDK/TestApiClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SDKExample.SDK 8 | { 9 | public class TestApiClient 10 | { 11 | readonly HttpClient _client; 12 | 13 | public TestApiClient(HttpClient httpClient) 14 | { 15 | _client = httpClient; 16 | } 17 | 18 | public async Task Test() 19 | { 20 | return await _client.GetAsync("/limit"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Extensions/HttpServer/EnableBufferingAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc.Filters; 3 | using System; 4 | 5 | namespace Xfrogcn.AspNetCore.Extensions 6 | { 7 | public class EnableBufferingAttribute : Attribute, IResourceFilter 8 | { 9 | public void OnResourceExecuted(ResourceExecutedContext context) 10 | { 11 | 12 | } 13 | 14 | public void OnResourceExecuting(ResourceExecutingContext context) 15 | { 16 | context.HttpContext.Request.EnableBuffering(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Extensions/AutoMapper/IMapperProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Xfrogcn.AspNetCore.Extensions 4 | { 5 | /// 6 | /// 映射转换 7 | /// 8 | public interface IMapperProvider 9 | { 10 | IServiceProvider ServiceProvider {get;} 11 | /// 12 | /// 获取TSource-->TTarget的映射转换器 13 | /// 14 | /// 源类型 15 | /// 目标类型 16 | /// 17 | IMapper GetMapper(); 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/IParallelQueueProducer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Xfrogcn.AspNetCore.Extensions 8 | { 9 | /// 10 | /// 队列生产者 11 | /// 12 | public interface IParallelQueueProducer 13 | { 14 | Task TryAddAsync(TEntity entity, CancellationToken token); 15 | 16 | Task<(TEntity, bool)> TryTakeAsync(TimeSpan timeout, CancellationToken token); 17 | 18 | Task StopAsync(CancellationToken token); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/ParallelQueueProducerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Xfrogcn.AspNetCore.Extensions 4 | { 5 | public class ParallelQueueProducerOptions 6 | { 7 | 8 | Func> _creator; 9 | public void SetProducer(Func> creator) 10 | { 11 | _creator = creator; 12 | } 13 | 14 | internal Func> GetCreator() 15 | { 16 | return _creator; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Extensions/WebApiStartupFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Xfrogcn.AspNetCore.Extensions 7 | { 8 | class WebApiStartupFilter : IStartupFilter 9 | { 10 | public Action Configure(Action next) 11 | { 12 | Action builder = (b) => 13 | { 14 | b.UseMiddleware(); 15 | next(b); 16 | }; 17 | 18 | return builder; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Extensions.RedisQueueProducer/QueueProducerServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection.Extensions; 2 | using Xfrogcn.AspNetCore.Extensions; 3 | using Xfrogcn.AspNetCore.Extensions.RedisQueueProducer; 4 | 5 | namespace Microsoft.Extensions.DependencyInjection 6 | { 7 | public static class QueueProducerServiceCollectionExtensions 8 | { 9 | public static ParallelQueueBuilder AddRedisQueueProducer(this ParallelQueueBuilder builder) 10 | { 11 | builder.Services.TryAddSingleton(); 12 | return builder; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/Memory/MemoryOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Xfrogcn.AspNetCore.Extensions; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection 7 | { 8 | public static class MemoryOptionsExtensions 9 | { 10 | public static ParallelQueueProducerOptions UseMemory( 11 | this ParallelQueueProducerOptions options) 12 | { 13 | options.SetProducer((sp, name) => 14 | { 15 | return new MemoryQueueProducer(); 16 | }); 17 | return options; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Extensions/Utils/HttpRequestExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace Xfrogcn.AspNetCore.Extensions 6 | { 7 | public static class HttpRequestExtensions 8 | { 9 | public static string GetAbsoluteUri(this HttpRequest request) 10 | { 11 | return new StringBuilder() 12 | .Append(request.Scheme) 13 | .Append("://") 14 | .Append(request.Host) 15 | .Append(request.PathBase) 16 | .Append(request.Path) 17 | .Append(request.QueryString) 18 | .ToString(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/IParallelQueueConsumerFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Xfrogcn.AspNetCore.Extensions 3 | { 4 | /// 5 | /// 并行队列消费者工厂 6 | /// 7 | public interface IParallelQueueConsumerFactory 8 | { 9 | /// 10 | /// 创建队列消费者 11 | /// 12 | /// 队列实体类型 13 | /// 状态类型 14 | /// 名称 15 | /// 状态 16 | /// 17 | IParallelQueueConsumer CreateConsumer(string name, TState state); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/QueueHandlers/QueueProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Xfrogcn.AspNetCore.Extensions 5 | { 6 | public class QueueProcessor 7 | { 8 | readonly QueueHandlerFactory _factory; 9 | public QueueProcessor(QueueHandlerFactory factory) 10 | { 11 | _factory = factory; 12 | } 13 | 14 | public async Task Process(TEntity msg, TState state, string name, Func errorHandler) 15 | { 16 | await _factory.Process(msg, state, name, errorHandler); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Extensions/Utils/IAutoRetry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Xfrogcn.AspNetCore.Extensions 5 | { 6 | public interface IAutoRetry 7 | { 8 | Task Retry(Func> proc, Func checkResult = null, int retryCount = 3, int delay = 100, bool throwError = true); 9 | TResult Retry(Func proc, Func checkResult = null, int retryCount = 3, int delay = 100, bool throwError = true); 10 | 11 | Task Retry(Func proc, int retryCount = 3, int delay = 100, bool throwError = true); 12 | 13 | void Retry(Action proc, int retryCount = 3, int delay = 100, bool throwError = true); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Extensions/Logger/LogPathAndTimeKey.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace Xfrogcn.AspNetCore.Extensions 6 | { 7 | 8 | internal class LogPathAndTimeKey : IEqualityComparer 9 | { 10 | public string Path { get; set; } 11 | 12 | public DateTimeOffset Time { get; set; } 13 | 14 | public bool Equals([AllowNull] LogPathAndTimeKey x, [AllowNull] LogPathAndTimeKey y) 15 | { 16 | return x.Path == y.Path; 17 | } 18 | 19 | public int GetHashCode([DisallowNull] LogPathAndTimeKey obj) 20 | { 21 | return (obj.Path ?? "").GetHashCode(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Extensions/AutoMapper/DefaultMapperProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace Xfrogcn.AspNetCore.Extensions 5 | { 6 | public class DefaultMapperProvider : IMapperProvider 7 | { 8 | private readonly IServiceProvider _serviceProvider; 9 | public DefaultMapperProvider(IServiceProvider serviceProvider) 10 | { 11 | _serviceProvider = serviceProvider; 12 | } 13 | 14 | public IServiceProvider ServiceProvider => _serviceProvider; 15 | 16 | public IMapper GetMapper() 17 | { 18 | return _serviceProvider.GetRequiredService>(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/IParallelQueueConsumer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Xfrogcn.AspNetCore.Extensions 6 | { 7 | /// 8 | /// 并行队列消费者 9 | /// 10 | /// 消息队列实体类型 11 | /// 状态数据类型 12 | public interface IParallelQueueConsumer 13 | { 14 | bool TryAdd(TEntity entity, TimeSpan timeout); 15 | 16 | Task StartAsync(); 17 | 18 | Task StopAsync(); 19 | 20 | long ExecutedCount { get; } 21 | 22 | double TotalLoad { get; } 23 | 24 | IReadOnlyList GetDetailLoadInfo(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/ParallelQueue/ParallelQueueExample/Handlers/MessageHandlerA.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Xfrogcn.AspNetCore.Extensions; 3 | 4 | namespace ParallelQueueExample.Handlers 5 | { 6 | /// 7 | /// 消息处理器,从QueueHandlerBase继承,实现对单个消息的处理 8 | /// 9 | class MessageHandlerA : QueueHandlerBase 10 | { 11 | /// 12 | /// 顺序,越小的排在前面 13 | /// 14 | public override int Order => base.Order; 15 | 16 | public override Task Process(QueueHandlerContext context) 17 | { 18 | // 示例: 演示消息处理管道 19 | context.Message.Output = context.Message.Input + 1; 20 | return base.Process(context); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/Http/ApiServer/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | 10 | namespace ApiServer 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Extensions/HttpClient/DisableEnsureSuccessStatusCodeMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Xfrogcn.AspNetCore.Extensions 7 | { 8 | /// 9 | /// 在请求管道中增加一个消息处理器,该处理器在请求消息中增加一个DisableEnsureSuccessStatusCode标记 10 | /// 在HttpClient扩展方法中读取此标记,如果存在,将忽略应答状态检查 11 | /// 12 | public class DisableEnsureSuccessStatusCodeMessageHandler : DelegatingHandler 13 | { 14 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 15 | { 16 | request.DisableEnsureSuccessStatusCode(); 17 | return base.SendAsync(request, cancellationToken); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Extensions.RedisQueueProducer/RedisOptions.cs: -------------------------------------------------------------------------------- 1 | using StackExchange.Redis; 2 | 3 | namespace Xfrogcn.AspNetCore.Extensions 4 | { 5 | public class RedisOptions 6 | { 7 | // 8 | // 摘要: 9 | // The configuration used to connect to Redis. 10 | public string Configuration { get; set; } 11 | // 12 | // 摘要: 13 | // The configuration used to connect to Redis. This is preferred over Configuration. 14 | public ConfigurationOptions ConfigurationOptions { get; set; } 15 | // 16 | // 摘要: 17 | // The Redis instance name. 18 | public string InstanceName { get; set; } 19 | 20 | /// 21 | /// 队列类型,暂不支持Bag 22 | /// 23 | public QueueType QueueType { get; set; } 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/Extensions.Tests/TokenProvider/MockTokenProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using Xfrogcn.AspNetCore.Extensions; 5 | 6 | namespace Extensions.Tests.TokenProvider 7 | { 8 | public class MockTokenProcessor : CertificateProcessor 9 | { 10 | internal int exeCount = 0; 11 | 12 | public override async Task GetToken(ClientCertificateInfo clientInfo, IHttpClientFactory clientFactory) 13 | { 14 | System.Threading.Interlocked.Increment(ref exeCount); 15 | await Task.Delay(100); 16 | return new ClientCertificateToken() 17 | { 18 | access_token = "1", 19 | expires_in = 60, 20 | token_type = "1" 21 | }; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Extensions/HttpClient/MessageHandlerServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection.Extensions; 2 | using Microsoft.Extensions.Http; 3 | using Xfrogcn.AspNetCore.Extensions; 4 | 5 | namespace Microsoft.Extensions.DependencyInjection 6 | { 7 | public static class MessageHandlerServiceCollectionExtensions 8 | { 9 | /// 10 | /// 注入HTTP请求日志记录 11 | /// 12 | /// 13 | /// 14 | public static IServiceCollection AddHttpMessageHandlerFilter(this IServiceCollection serviceDescriptors) 15 | { 16 | serviceDescriptors.TryAddEnumerable(ServiceDescriptor.Singleton()); 17 | return serviceDescriptors; 18 | } 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Extensions/Logger/TestLogContent.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Xfrogcn.AspNetCore.Extensions 7 | { 8 | public class TestLogContent 9 | { 10 | private readonly List _logList = new List(); 11 | 12 | 13 | public IReadOnlyList LogContents => _logList; 14 | 15 | internal void AddLogItem(TestLogItem logItem) 16 | { 17 | _logList.Add(logItem); 18 | } 19 | } 20 | 21 | public class TestLogItem 22 | { 23 | public string CategoryName { get; set; } 24 | public string Message { get; set; } 25 | 26 | public LogLevel LogLevel { get; set; } 27 | public EventId EventId { get; set; } 28 | 29 | public List ScopeValues { get; set; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Extensions/Utils/ObjectExtensions.cs: -------------------------------------------------------------------------------- 1 | using Xfrogcn.BinaryFormatter; 2 | 3 | namespace Xfrogcn.AspNetCore.Extensions 4 | { 5 | public static class ObjectExtensions 6 | { 7 | public static object GetEntity(this byte[] bytes) 8 | { 9 | if (bytes == null || bytes.Length == 0) 10 | { 11 | return default; 12 | } 13 | 14 | return BinarySerializer.Deserialize(bytes); 15 | } 16 | 17 | public static T GetEntity(this byte[] bytes) 18 | { 19 | if (bytes == null || bytes.Length == 0) 20 | { 21 | return default; 22 | } 23 | 24 | return BinarySerializer.Deserialize(bytes); 25 | } 26 | 27 | public static byte[] GetBytes(this object entity) 28 | { 29 | return BinarySerializer.Serialize(entity); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/Http/ApiServer/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:35851", 8 | "sslPort": 0 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "weatherforecast", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "ApiServer": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "weatherforecast", 24 | "applicationUrl": "http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Extensions/Utils/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Xfrogcn.AspNetCore.Extensions 7 | { 8 | public static class StringExtensions 9 | { 10 | private static Random random = new Random(); 11 | const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"; 12 | const string numbericChars = "0123456789"; 13 | public static string RandomString(int length) 14 | { 15 | return new string(Enumerable.Repeat(chars, length) 16 | .Select(s => s[random.Next(s.Length)]).ToArray()); 17 | } 18 | 19 | public static string RandomNumbericString(int length) 20 | { 21 | return new string(Enumerable.Repeat(numbericChars, length) 22 | .Select(s => s[random.Next(s.Length)]).ToArray()); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/ParallelQueue/ParallelQueueExample/Handlers/MessageHandlerB.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System.Threading.Tasks; 3 | using Xfrogcn.AspNetCore.Extensions; 4 | 5 | namespace ParallelQueueExample.Handlers 6 | { 7 | class MessageHandlerB : QueueHandlerBase 8 | { 9 | readonly ILogger _logger; 10 | public MessageHandlerB(ILogger logger) 11 | { 12 | _logger = logger; 13 | } 14 | 15 | // 排在A的后面 16 | public override int Order => base.Order + 1; 17 | 18 | public override async Task Process(QueueHandlerContext context) 19 | { 20 | context.Message.Output = context.Message.Output + 1; 21 | 22 | _logger.LogInformation("{name}-->{output}", context.QueueName, context.Message.Output); 23 | 24 | await Task.Delay(500); 25 | 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/FactoryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Xfrogcn.AspNetCore.Extensions 3 | { 4 | public static class FactoryExtensions 5 | { 6 | public static IParallelQueueConsumer CreateConsumer(this IParallelQueueConsumerFactory factory, string name) 7 | { 8 | return factory.CreateConsumer(name, null); 9 | } 10 | 11 | public static IParallelQueueConsumer CreateConsumer(this IParallelQueueConsumerFactory factory) 12 | { 13 | return factory.CreateConsumer(string.Empty, null); 14 | } 15 | 16 | public static IParallelQueueConsumer CreateConsumer(this IParallelQueueConsumerFactory factory, TState state) 17 | { 18 | return factory.CreateConsumer(string.Empty, state); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /doc/HttpMessageExtensions.md: -------------------------------------------------------------------------------- 1 | # Http请求与应答消息扩展 2 | 3 | ## HttpResponseMessage.GetObjectAsync<TResponse> 4 | 5 | Task<TResponse>> GetObjectAsync<TResponse>(this HttpResponseMessage response, bool copy = false) 6 | 7 | 从HttpResponseMessage消息中去读指定类型的应答内容 8 | 9 | - copy:bool类型,是否拷贝内容,默认false 10 | 11 | 12 | ## HttpRequestMessage.GetObjectAsync<TResponse> 13 | 14 | Task<TResponse> GetObjectAsync<TResponse>(this HttpRequestMessage request, bool copy = false) 15 | 16 | 从HttpRequestMessage消息中获取指定类型的请求内容 17 | 18 | - copy:bool类型,是否拷贝内容,默认false 19 | 20 | ## HttpResponseMessage.WriteObjectAsync 21 | 22 | Task WriteObjectAsync(this HttpResponseMessage rsponse, object body) 23 | 24 | 将body对象写入HttpResponseMessage 25 | 26 | - body: object类型,写入对象 27 | 28 | ## HttpRequestMessage.WriteObjectAsync 29 | 30 | Task WriteObjectAsync(this HttpRequestMessage request, object body) 31 | 32 | 将body对象写入HttpRequestMessage 33 | 34 | - body: object类型,写入对象 35 | -------------------------------------------------------------------------------- /src/Extensions/Logger/TestLoggerProviderServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using Microsoft.Extensions.Logging; 4 | using Xfrogcn.AspNetCore.Extensions; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection 7 | { 8 | public static class TestLoggerProviderServiceCollectionExtensions 9 | { 10 | public static IServiceCollection AddTestLoggerProvider(this IServiceCollection serviceDescriptors) 11 | { 12 | serviceDescriptors.AddLogging(); 13 | serviceDescriptors.TryAddSingleton(); 14 | serviceDescriptors.AddSingleton(); 15 | return serviceDescriptors; 16 | } 17 | public static TestLogContent GetTestLogContent(this IServiceProvider provider) 18 | { 19 | return provider.GetRequiredService(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/Http/HttpHeaderTracking/ServiceBFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.AspNetCore.Mvc.Testing; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace HttpHeaderTracking 8 | { 9 | class ServiceBFactory : WebApplicationFactory 10 | where TEntryPoint : class 11 | { 12 | 13 | public ServiceBFactory() 14 | { 15 | 16 | } 17 | 18 | 19 | protected override void ConfigureWebHost(IWebHostBuilder builder) 20 | { 21 | base.ConfigureWebHost(builder); 22 | builder.UseExtensions(null, config => 23 | { 24 | // 将服务端请求日志层级设置为Verbose可记录服务端请求详细日志 25 | config.FileLog = false; 26 | config.ConsoleLog = true; 27 | config.AppLogLevel = Serilog.Events.LogEventLevel.Warning; 28 | }); 29 | 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/Logging/NormalFileLogging/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | using System; 5 | 6 | namespace NormalFileLogging 7 | { 8 | class Program 9 | { 10 | static void Main(string[] args) 11 | { 12 | using IHost host = Host.CreateDefaultBuilder() 13 | .UseExtensions(config => 14 | { 15 | // 默认开启 16 | // config.FileLog = true; 17 | config.AppLogLevel = Serilog.Events.LogEventLevel.Verbose; 18 | }) 19 | .Build(); 20 | 21 | _ = host.StartAsync(); 22 | 23 | var logger = host.Services.GetRequiredService>(); 24 | // 放入 ./Logs/[YYYY-MM-DD]/Program.log中 25 | logger.LogInformation("测试日志:{time}", DateTimeOffset.Now); 26 | 27 | Console.ReadLine(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/Http/RequestLog/TestWebApplicationFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.AspNetCore.Mvc.Testing; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net.Http; 6 | using System.Text; 7 | 8 | namespace RequestLog 9 | { 10 | class TestWebApplicationFactory : WebApplicationFactory 11 | where TEntryPoint: class 12 | { 13 | protected override void ConfigureWebHost(IWebHostBuilder builder) 14 | { 15 | builder = builder.UseExtensions(null, config => 16 | { 17 | // 将服务端请求日志层级设置为Verbose可记录服务端请求详细日志 18 | config.ServerRequestLevel = Serilog.Events.LogEventLevel.Verbose; 19 | config.AppLogLevel = Serilog.Events.LogEventLevel.Debug; 20 | config.FileLog = false; 21 | config.ConsoleLog = true; 22 | 23 | }); 24 | base.ConfigureWebHost(builder); 25 | 26 | } 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/Memory/MemoryQueueProducer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Xfrogcn.AspNetCore.Extensions 7 | { 8 | public class MemoryQueueProducer : IParallelQueueProducer 9 | { 10 | private readonly BlockingCollection _queue = new BlockingCollection(); 11 | 12 | public Task StopAsync(CancellationToken token) 13 | { 14 | return Task.CompletedTask; 15 | } 16 | 17 | public Task TryAddAsync(TEntity entity, CancellationToken token) 18 | { 19 | bool isOK = _queue.TryAdd(entity); 20 | return Task.FromResult(isOK); 21 | } 22 | 23 | public Task<(TEntity, bool)> TryTakeAsync(TimeSpan timeout, CancellationToken token) 24 | { 25 | bool isOk = _queue.TryTake(out TEntity item, (int)timeout.TotalMilliseconds, token); 26 | return Task.FromResult((item, isOk)); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Extensions.RedisQueueProducer/OptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using Xfrogcn.AspNetCore.Extensions; 4 | using Xfrogcn.AspNetCore.Extensions.RedisQueueProducer; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection 7 | { 8 | public static class OptionsExtensions 9 | { 10 | public static ParallelQueueProducerOptions UseRedis( 11 | this ParallelQueueProducerOptions options, 12 | Action redisOptions) 13 | { 14 | options.SetProducer((sp, name) => 15 | { 16 | RedisOptions ro = new RedisOptions(); 17 | redisOptions?.Invoke(ro); 18 | ILogger> logger = sp.GetRequiredService>>(); 19 | RedisConnectionManager connManager = sp.GetRequiredService(); 20 | return new QueueProducer(name, ro, connManager, logger ); 21 | }); 22 | return options; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.0.0 4 | 王海波 5 | 王海波 6 | https://github.com/xfrogcn/Xfrogcn.AspNetCore.Extensions 7 | git 8 | true 9 | snupkg 10 | package-icon.png 11 | 12 | 13 | 14 | Xfrogcn AspNetCore Extensions 15 | 16 | 17 | MIT 18 | 19 | 20 | xfrogcn@163.com 21 | 22 | true 23 | 24 | 25 | 26 | True 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 无叶菜 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Extensions/Utils/DistributedCacheExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Xfrogcn.AspNetCore.Extensions; 4 | 5 | namespace Microsoft.Extensions.Caching.Distributed 6 | { 7 | public static class DistributedCacheExtensions 8 | { 9 | private static JsonHelper jsonHelper = new JsonHelper(); 10 | public static async Task GetAsync(this IDistributedCache cache, string key, CancellationToken cancellationToken = default) 11 | { 12 | var bytes = await cache.GetAsync(key, cancellationToken); 13 | if(bytes == null || bytes.Length==0) 14 | { 15 | return default(TEntity); 16 | } 17 | 18 | 19 | return (TEntity)bytes.GetEntity(); 20 | } 21 | 22 | 23 | 24 | public static async Task SetAsync(this IDistributedCache cache, string key, TEntity obj, CancellationToken cancellationToken = default) 25 | { 26 | byte[] bytes = obj.GetBytes(); 27 | 28 | await cache.SetAsync(key, bytes, cancellationToken); 29 | } 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Extensions.RedisQueueProducer/Xfrogcn.AspNetCore.Extensions.RedisQueueProducer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | Xfrogcn.AspNetCore.Extensions.RedisQueueProducer 6 | Xfrogcn.AspNetCore.Extensions 7 | true 8 | parallel queue producer for redis MQ 9 | parallel queue producer for redis MQ 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Extensions/LogPathTemplates.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Xfrogcn.AspNetCore.Extensions 3 | { 4 | public static class LogPathTemplates 5 | { 6 | /// 7 | /// 以每天日期为目录,日志名称为文件名 8 | /// 9 | public static readonly string DayFolderAndLoggerNameFile = "{Timestamp:yyyy-MM-dd}" + System.IO.Path.DirectorySeparatorChar +"{SourceContext}.log"; 10 | /// 11 | /// 以每天日期为日志名称 12 | /// 13 | public static readonly string DayFile = "{Timestamp:yyyy-MM-dd}.log"; 14 | /// 15 | /// 以[日志名称_每天日志]为日志文件名称 16 | /// 17 | public static readonly string LoggerNameAndDayFile = "{SourceContext}_{Timestamp:yyyy-MM-dd}.log"; 18 | /// 19 | /// 以日志级别缩写为日志文件名称 20 | /// 21 | public static readonly string LevelFile = "{Level:u3}.log"; 22 | /// 23 | /// 以每天日期为目录,日志级别缩写为日志名称 24 | /// 25 | public static readonly string DayFolderAndLevelFile = "{Timestamp:yyyy-MM-dd}" + System.IO.Path.DirectorySeparatorChar + "{Level:u3}.log"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Extensions/Entities/PagingRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace Xfrogcn.AspNetCore.Extensions 7 | { 8 | public class PagingRequest 9 | { 10 | [JsonPropertyName("pageSize")] 11 | public int PageSize { get; set; } 12 | 13 | [JsonPropertyName("pageIndex")] 14 | public int PageIndex { get; set; } 15 | 16 | 17 | public void Normalize(int maxSize = 1000, int defaultSize = 20) 18 | { 19 | if(PageSize<0 || PageSize>maxSize) 20 | { 21 | PageSize = defaultSize; 22 | } 23 | if (PageIndex <= 0) 24 | { 25 | PageIndex = 1; 26 | } 27 | } 28 | 29 | 30 | public int Skip 31 | { 32 | get 33 | { 34 | return (int)((PageIndex - 1) * PageSize); 35 | } 36 | } 37 | 38 | public void SetToResponse(PagingResponseMessage r) 39 | { 40 | r.PageIndex = PageIndex; 41 | r.PageSize = PageSize; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Extensions/HttpClient/MockHttpMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Xfrogcn.AspNetCore.Extensions 9 | { 10 | 11 | public class MockHttpMessageHandler : HttpMessageHandler 12 | { 13 | readonly MockHttpMessageHandlerOptions _options = null; 14 | public MockHttpMessageHandler(MockHttpMessageHandlerOptions options) 15 | { 16 | _options = options; 17 | } 18 | 19 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 20 | { 21 | HttpResponseMessage r = new HttpResponseMessage(); 22 | foreach (MockHttpMessageHandlerOptions.MockItem mi in _options.MockList) 23 | { 24 | bool canProc = mi.Predicate(request); 25 | if (canProc) 26 | { 27 | await mi.Proc(request, r); 28 | break; 29 | } 30 | } 31 | return r; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/QueueExecutorLoadInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace Xfrogcn.AspNetCore.Extensions 3 | { 4 | /// 5 | /// 队列执行器负载情况 6 | /// 7 | public class QueueExecutorLoadInfo 8 | { 9 | /// 10 | /// 空闲时间 11 | /// 12 | public double Idle { get; set; } 13 | /// 14 | /// 执行时间 15 | /// 16 | public double Busy { get; set; } 17 | /// 18 | /// 负载率 Busy / (Idle + Busy) 19 | /// 20 | public double LoadRatio { get; set; } 21 | 22 | /// 23 | /// 区间内执行计数 24 | /// 25 | public long Counter { get; set; } 26 | 27 | /// 28 | /// 创建空负载 29 | /// 30 | /// 31 | public static QueueExecutorLoadInfo CreateEmptyLoad(TimeSpan period) 32 | { 33 | return new QueueExecutorLoadInfo() 34 | { 35 | Idle = period.TotalMilliseconds, 36 | Busy = 0, 37 | LoadRatio = 0 38 | }; 39 | } 40 | 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/Logging/JsonConsoleLogging/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | using System; 5 | using System.Drawing; 6 | using Xfrogcn.AspNetCore.Extensions; 7 | 8 | namespace JsonConsoleLogging 9 | { 10 | class Program 11 | { 12 | static void Main(string[] args) 13 | { 14 | using IHost host = Host.CreateDefaultBuilder() 15 | .UseExtensions(config => 16 | { 17 | config.ConsoleJsonLog = true; 18 | config.FileLog = false; 19 | config.AppLogLevel = Serilog.Events.LogEventLevel.Verbose; 20 | }) 21 | .Build(); 22 | 23 | _ = host.StartAsync(); 24 | 25 | var logger = host.Services.GetRequiredService>(); 26 | logger.LogInformation("测试日志:{time}", DateTimeOffset.Now); 27 | // 使用@前缀将对象合并大日志JSON对象中,如果不带@前缀,则以字符串方式进行处理 28 | logger.LogInformation("obj: {@obj}", new Point { X = 0, Y = 1 }); 29 | 30 | Console.ReadLine(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /doc/TokenRequest.md: -------------------------------------------------------------------------------- 1 | # Http认证令牌处理 2 | 3 | 扩展库通过令牌提供器,并与HttpClient集合,实现了HttpClient请求认证的自动化处理,大大简化了认证的过程。 4 | 5 | 有关令牌提供器的详细信息,请参见[令牌提供器](./TokenProvider.md) 6 | 7 | 要使用Http认证令牌处理,可分为两个步骤: 8 | 9 | - 通过令牌提供器配置认证信息 10 | - 配置对应名称的HttpClient使用相应的认证配置 11 | 12 | HttpClient与令牌提供器之间通过指定的ClientID关联 13 | 14 | ## 示例 15 | 16 | 以下配置ClientID为TestClient的客户端使用基本认证(用户名为test,密码为test),并设置默认HttpClient(名称为"")自动使用此认证信息管理认证过程。 17 | 18 | ```c# 19 | // 1. 首先,需要加入客户端信息,每个客户端必须有唯一的ID 20 | sc.AddClientTokenProvider(options => 21 | { 22 | options.AddClient("", "TestClient", "") 23 | // 设置此客户端使用Basic认证 24 | .UseBasicAuth("test", "test"); 25 | }); 26 | // 2. 设置HttpClient关联认证管理器 27 | sc.AddHttpClient("", client => 28 | { 29 | client.BaseAddress = new Uri("http://localhost"); 30 | }) 31 | // 设置此客户端关联TestClient的认证配置 32 | .AddTokenMessageHandler("TestClient"); 33 | 34 | 35 | // 3. 使用 36 | IServiceProvider sp = sc.BuildServiceProvider(); 37 | 38 | IHttpClientFactory httpFactory = sp.GetRequiredService(); 39 | var client = httpFactory.CreateClient(""); 40 | string response = await client.GetAsync("/limit"); 41 | ``` 42 | 43 | 有关示例,请参考示例项目`examples/Http/BasicAuth` -------------------------------------------------------------------------------- /src/Extensions/Entities/ResponseMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Text.Json.Serialization; 5 | 6 | namespace Xfrogcn.AspNetCore.Extensions 7 | { 8 | /// 9 | /// 应答消息 10 | /// 11 | public class ResponseMessage 12 | { 13 | [JsonPropertyName("code")] 14 | public string Code { get; set; } 15 | 16 | [JsonPropertyName("message")] 17 | public string Message { get; set; } 18 | 19 | public ResponseMessage() 20 | { 21 | Code = "0"; 22 | } 23 | 24 | public bool IsSuccess => Code == "0"; 25 | } 26 | 27 | public class ResponseMessage : ResponseMessage 28 | { 29 | [JsonPropertyName("extension")] 30 | public TData Extension { get; set; } 31 | } 32 | 33 | public class PagingResponseMessage: ResponseMessage> 34 | { 35 | [JsonPropertyName("pageSize")] 36 | public long PageSize { get; set; } 37 | 38 | [JsonPropertyName("pageIndex")] 39 | public long PageIndex { get; set; } 40 | 41 | [JsonPropertyName("total")] 42 | public long Total { get; set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Extensions/Xfrogcn.AspNetCore.Extensions.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.808.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xfrogcn.AspNetCore.Extensions", "Xfrogcn.AspNetCore.Extensions.csproj", "{A6E61AC6-3E36-4C64-AAE6-CB796CD65430}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {A6E61AC6-3E36-4C64-AAE6-CB796CD65430}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {A6E61AC6-3E36-4C64-AAE6-CB796CD65430}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {A6E61AC6-3E36-4C64-AAE6-CB796CD65430}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {A6E61AC6-3E36-4C64-AAE6-CB796CD65430}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {9818F971-2D56-41F7-9B50-5BD314E3252D} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/Extensions/Logger/LogLevelConverter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Serilog.Events; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace Xfrogcn.AspNetCore.Extensions 8 | { 9 | public static class LogLevelConverter 10 | { 11 | public static LogLevel Converter(LogEventLevel level) 12 | { 13 | LogLevel l = LogLevel.Trace; 14 | switch (level) 15 | { 16 | case LogEventLevel.Debug: 17 | l = LogLevel.Debug; 18 | break; 19 | case LogEventLevel.Error: 20 | l = LogLevel.Error; 21 | break; 22 | case LogEventLevel.Fatal: 23 | l = LogLevel.Critical; 24 | break; 25 | case LogEventLevel.Information: 26 | l = LogLevel.Information; 27 | break; 28 | case LogEventLevel.Verbose: 29 | l = LogLevel.Trace; 30 | break; 31 | case LogEventLevel.Warning: 32 | l = LogLevel.Warning; 33 | break; 34 | } 35 | 36 | return l; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Extensions/AutoMapper/IMapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Reflection; 5 | using System.Text; 6 | using Xfrogcn.AspNetCore.Extensions.AutoMapper; 7 | 8 | namespace Xfrogcn.AspNetCore.Extensions 9 | { 10 | public interface IMapper 11 | { 12 | /// 13 | /// 将source转换为TTarget类型 14 | /// 15 | /// 源 16 | /// 17 | TTarget Convert(TSource source, CircularRefChecker checker = null); 18 | 19 | /// 20 | /// 拷贝 21 | /// 22 | /// 23 | /// 24 | void CopyTo(TSource source, TTarget target, CircularRefChecker checker = null); 25 | 26 | /// 27 | /// 获取一个除去传入属性列表的CopyTo Action 28 | /// 29 | /// 30 | /// 31 | Action DefineCopyTo(Expression> excludeProperties); 32 | 33 | Action GenerateDefaultCopyToDelegateWithExclude(Dictionary excludeProperties); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/Http/SDKExample/SDK/SDKServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using SDKExample.SDK; 2 | 3 | namespace Microsoft.Extensions.DependencyInjection 4 | { 5 | public static class SDKServiceCollectionExtensions 6 | { 7 | /// 8 | /// 添加TestSDK 9 | /// 此SDK的服务端使用Basic验证 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | public static IHttpClientBuilder AddTestSDK(this IServiceCollection serviceDescriptors, string url, string clientId, string userName, string password) 18 | { 19 | // 加入SDK所使用的HttpClient客户端 20 | serviceDescriptors.AddTokenClient(url, clientId, "", options=> 21 | { 22 | options.UseBasicAuth(userName, password); 23 | }); 24 | 25 | return serviceDescriptors.AddHttpClient(clientId, client => 26 | { 27 | client.BaseAddress = new System.Uri(url); 28 | }) 29 | .AddTypedClient() 30 | .AddTokenMessageHandler(clientId); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/Http/ApiServer/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace ApiServer.Controllers 9 | { 10 | [ApiController] 11 | [Route("[controller]")] 12 | public class WeatherForecastController : ControllerBase 13 | { 14 | private static readonly string[] Summaries = new[] 15 | { 16 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 17 | }; 18 | 19 | private readonly ILogger _logger; 20 | 21 | public WeatherForecastController(ILogger logger) 22 | { 23 | _logger = logger; 24 | } 25 | 26 | [HttpGet] 27 | public IEnumerable Get() 28 | { 29 | var rng = new Random(); 30 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 31 | { 32 | Date = DateTime.Now.AddDays(index), 33 | TemperatureC = rng.Next(-20, 55), 34 | Summary = Summaries[rng.Next(Summaries.Length)] 35 | }) 36 | .ToArray(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Extensions/AutoMapper/MapperServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | using Xfrogcn.AspNetCore.Extensions; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection 7 | { 8 | public static class MapperServiceCollectionExtensions 9 | { 10 | public static IServiceCollection AddLightweightMapper(this IServiceCollection serviceDescriptors, Action options=null) 11 | { 12 | serviceDescriptors.TryAddSingleton(); 13 | serviceDescriptors.TryAddSingleton(typeof(IMapper<,>), typeof(DefaultMapper<,>)); 14 | serviceDescriptors.Configure(o => 15 | { 16 | options?.Invoke(o); 17 | }); 18 | return serviceDescriptors; 19 | } 20 | 21 | 22 | public static IServiceCollection ConfigureLightweightMapper(this IServiceCollection serviceDescriptors, Action options) 23 | { 24 | serviceDescriptors.AddLightweightMapper(); 25 | if (options != null) 26 | { 27 | serviceDescriptors.Configure(options); 28 | } 29 | return serviceDescriptors; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/Http/SDKExample/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Testing; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using SDKExample.SDK; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace SDKExample 8 | { 9 | class Program 10 | { 11 | static async Task Main(string[] args) 12 | { 13 | WebApplicationFactory server = new WebApplicationFactory(); 14 | 15 | IServiceCollection sc = new ServiceCollection() 16 | .AddExtensions(null, config => 17 | { 18 | config.FileLog = false; 19 | config.ConsoleLog = true; 20 | }); 21 | 22 | 23 | sc.AddTestSDK("http://localhost", "TEST_SDK", "test", "test"); 24 | 25 | // 此处是演示代码,设置HttpClient请求模拟的服务端,正式使用无需此设置 26 | sc.AddHttpClient("TEST_SDK") 27 | .ConfigurePrimaryHttpMessageHandler(() => 28 | { 29 | return server.Server.CreateHandler(); 30 | }); 31 | 32 | var sp = sc.BuildServiceProvider(); 33 | 34 | var client = sp.GetRequiredService(); 35 | 36 | string response = await client.Test(); 37 | 38 | Console.WriteLine($"应答:{response}"); 39 | 40 | Console.ReadLine(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/Http/ApiServer/Controllers/TrackingController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.Logging; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | 6 | namespace ApiServer.Controllers 7 | { 8 | [ApiController] 9 | [Route("{controller}")] 10 | public class TrackingController : ControllerBase 11 | { 12 | private readonly ILogger _logger; 13 | private readonly IHttpClientFactory _httpFactory; 14 | 15 | public TrackingController( 16 | ILogger logger, 17 | IHttpClientFactory httpFactory) 18 | { 19 | _logger = logger; 20 | _httpFactory = httpFactory; 21 | } 22 | 23 | [HttpGet("serviceA")] 24 | public async Task ServiceA() 25 | { 26 | _logger.LogWarning("ServiceA x-request-id: {requestId}", HttpContext.Request.Headers["x-request-id"]); 27 | var client = _httpFactory.CreateClient(); 28 | var response = await client.GetAsync("/tracking/serviceB"); 29 | return $"ServiceA --> {response}"; 30 | } 31 | 32 | [HttpGet("serviceB")] 33 | public string ServiceB() 34 | { 35 | _logger.LogWarning("ServiceB x-request-id: {requestId}", HttpContext.Request.Headers["x-request-id"]); 36 | return "ServiceB"; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/test/Extensions.Tests/Extensions.Tests.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/test/Extensions.Tests/Extensions.Tests.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/test/Extensions.Tests/Extensions.Tests.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /test/Extensions.Tests/Extensions.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | Library 8 | 9 | 10 | 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Extensions/AutoMapper/IMapperProviderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | 5 | namespace Xfrogcn.AspNetCore.Extensions 6 | { 7 | public static class IMapperProviderExtensions 8 | { 9 | public static TTarget Convert(this IMapperProvider provider, TSource sourceObj) 10 | { 11 | var mapper = provider.GetMapper(); 12 | return mapper.Convert(sourceObj); 13 | } 14 | 15 | public static List ConvertList(this IMapperProvider provider, List sourceObj) 16 | { 17 | var mapper = provider.GetMapper, List>(); 18 | return mapper.Convert(sourceObj); 19 | } 20 | 21 | 22 | public static void CopyTo(this IMapperProvider provider, TSource sourceObj, TTarget targetObj) 23 | { 24 | var mapper = provider.GetMapper(); 25 | mapper.CopyTo(sourceObj, targetObj); 26 | } 27 | 28 | public static Action DefineCopyTo(this IMapperProvider provider, Expression> excludeProperties = null) 29 | where TSource: class 30 | where TTarget : class 31 | { 32 | var mapper = provider.GetMapper(); 33 | return mapper.DefineCopyTo(excludeProperties); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/Logging/NormalConsoleLogging/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using System; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Logging; 6 | using Xfrogcn.AspNetCore.Extensions; 7 | 8 | namespace NormalConsoleLogging 9 | { 10 | class Program 11 | { 12 | static void Main(string[] args) 13 | { 14 | using IHost host = Host.CreateDefaultBuilder() 15 | .UseExtensions(config => 16 | { 17 | config.ConsoleLog = true; 18 | config.FileLog = false; 19 | config.AppLogLevel = Serilog.Events.LogEventLevel.Verbose; 20 | }) 21 | .Build(); 22 | 23 | _ = host.StartAsync(); 24 | 25 | var logger = host.Services.GetRequiredService>(); 26 | logger.LogInformation("测试日志:{time}", DateTimeOffset.Now); 27 | 28 | // 动态修改日志级别 29 | var apiConfig = host.Services.GetRequiredService(); 30 | apiConfig.AppLogLevel = Serilog.Events.LogEventLevel.Error; 31 | // 此日志被忽略 32 | logger.LogInformation("测试日志:{time}", DateTimeOffset.Now); 33 | apiConfig.AppLogLevel = Serilog.Events.LogEventLevel.Verbose; 34 | 35 | // 日志显示 36 | logger.LogInformation("测试日志:{time}", DateTimeOffset.Now); 37 | 38 | 39 | Console.ReadLine(); 40 | 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/Http/HttpHeaderTracking/ServiceAFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.AspNetCore.Mvc.Testing; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System; 5 | 6 | namespace HttpHeaderTracking 7 | { 8 | class ServiceAFactory : WebApplicationFactory 9 | where TEntryPoint : class 10 | { 11 | readonly WebApplicationFactory _factoryB; 12 | public ServiceAFactory(WebApplicationFactory factoryB) 13 | { 14 | _factoryB = factoryB; 15 | } 16 | 17 | 18 | protected override void ConfigureWebHost(IWebHostBuilder builder) 19 | { 20 | base.ConfigureWebHost(builder); 21 | builder.UseExtensions(null, config => 22 | { 23 | // 将服务端请求日志层级设置为Verbose可记录服务端请求详细日志 24 | config.FileLog = false; 25 | config.ConsoleLog = true; 26 | config.AppLogLevel = Serilog.Events.LogEventLevel.Warning; 27 | }) 28 | .ConfigureServices(services=> 29 | { 30 | // 配置ServiceA所使用的HttpClient 31 | services.AddHttpClient("", client => 32 | { 33 | client.BaseAddress = new Uri("http://localhost"); 34 | }) 35 | .ConfigurePrimaryHttpMessageHandler(() => 36 | { 37 | return _factoryB.Server.CreateHandler(); 38 | }); 39 | }); 40 | 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/test/Extensions.Tests/bin/Debug/netcoreapp3.1/Extensions.Tests.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/test/Extensions.Tests", 16 | "stopAtEntry": false, 17 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser 18 | "serverReadyAction": { 19 | "action": "openExternally", 20 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 21 | }, 22 | "env": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "sourceFileMap": { 26 | "/Views": "${workspaceFolder}/Views" 27 | } 28 | }, 29 | { 30 | "name": ".NET Core Attach", 31 | "type": "coreclr", 32 | "request": "attach", 33 | "processId": "${command:pickProcess}" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/ParallelQueueConsumerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace Xfrogcn.AspNetCore.Extensions 5 | { 6 | /// 7 | /// 并行队列处理器配置 8 | /// 9 | public class ParallelQueueConsumerOptions 10 | { 11 | /// 12 | /// 执行器数量 13 | /// 14 | public int ExecutorCount { get; set; } = 5; 15 | 16 | /// 17 | /// 执行器队列容量 18 | /// 19 | public int ExecutorQueueCapacity { get; set; } = 1; 20 | 21 | /// 22 | /// 执行器队列类型 23 | /// 24 | public QueueType ExecutorQueueType { get; set; } = QueueType.FIFO; 25 | 26 | /// 27 | /// 默认等待队列超时时间 28 | /// 29 | public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromSeconds(10); 30 | 31 | /// 32 | /// 处理委托 33 | /// 34 | public Func ExecuteDelegate { get; set; } 35 | 36 | /// 37 | /// 统计周期 38 | /// 最小10秒 39 | /// 40 | public TimeSpan StatisticalPeriod { get; set; } = TimeSpan.FromMinutes(1); 41 | } 42 | 43 | public enum QueueType 44 | { 45 | /// 46 | /// 先进先出 47 | /// 48 | FIFO, 49 | /// 50 | /// 后进先出 51 | /// 52 | LIFO, 53 | /// 54 | /// 无序 55 | /// 56 | Bag 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Extensions/AutoMapper/MapperOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Xfrogcn.AspNetCore.Extensions 6 | { 7 | public class MapperOptions 8 | { 9 | public class MapperItem 10 | { 11 | public Type SourceType { get; set; } 12 | 13 | public Type TargetType { get; set; } 14 | 15 | } 16 | 17 | public class MapperItem : MapperItem 18 | { 19 | public Action Convert { get; set; } 20 | } 21 | 22 | private readonly List _mapperList = new List(); 23 | 24 | public IReadOnlyList MapperList => _mapperList; 25 | 26 | public void AddConvert(Action converter) 27 | { 28 | if (converter != null) 29 | { 30 | _mapperList.Add(new MapperItem() 31 | { 32 | SourceType = typeof(TSource), 33 | TargetType = typeof(TTarget), 34 | Convert = converter 35 | }); 36 | } 37 | } 38 | 39 | public IReadOnlyList GetConverter() 40 | { 41 | var sType = typeof(TSource); 42 | var tType = typeof(TTarget); 43 | // 能赋给TSource的,并且 44 | var list = _mapperList.Where(m => m.SourceType.IsAssignableFrom(sType) 45 | && m.TargetType.IsAssignableFrom(tType)).ToList(); 46 | return list; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/DefaultParallelQueueProducerFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Options; 3 | using System; 4 | using System.Collections.Concurrent; 5 | 6 | namespace Xfrogcn.AspNetCore.Extensions 7 | { 8 | public class DefaultParallelQueueProducerFactory : IParallelQueueProducerFactory 9 | { 10 | readonly IServiceProvider _serviceProvider; 11 | private ConcurrentDictionary _cache = new ConcurrentDictionary(); 12 | public DefaultParallelQueueProducerFactory( 13 | IServiceProvider serviceProvider) 14 | { 15 | _serviceProvider = serviceProvider; 16 | } 17 | public IParallelQueueProducer CreateProducer(string queueName) 18 | { 19 | return _cache.GetOrAdd(queueName, (key) => 20 | { 21 | using (var scope = _serviceProvider.CreateScope()) 22 | { 23 | var options = scope.ServiceProvider.GetService>>(); 24 | if (options == null || options.Value == null) 25 | { 26 | throw new InvalidOperationException("未配置队列生产者"); 27 | } 28 | var producer = options.Get(queueName).GetCreator(); 29 | if (producer == null) 30 | { 31 | throw new InvalidOperationException($"未配置队列:{queueName}"); 32 | } 33 | return producer(scope.ServiceProvider, queueName); 34 | } 35 | }) as IParallelQueueProducer; 36 | 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/ParallelQueueServiceConnectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection.Extensions; 2 | using Xfrogcn.AspNetCore.Extensions; 3 | 4 | namespace Microsoft.Extensions.DependencyInjection 5 | { 6 | public static class ParallelQueueServiceCollectionionExtensions 7 | { 8 | public static ParallelQueueBuilder AddParallelQueue(this IServiceCollection serviceDescriptors, string name) 9 | { 10 | serviceDescriptors.AddOptions(); 11 | serviceDescriptors.TryAddSingleton(); 12 | serviceDescriptors.TryAddSingleton(); 13 | ParallelQueueBuilder builder = new ParallelQueueBuilder(name, serviceDescriptors); 14 | return builder; 15 | } 16 | 17 | public static ParallelQueueBuilder AddParallelQueue(this IServiceCollection serviceDescriptors, string name) 18 | { 19 | return AddParallelQueue(serviceDescriptors, name); 20 | } 21 | 22 | public static ParallelQueueBuilder AddParallelQueue(this IServiceCollection serviceDescriptors) 23 | { 24 | return AddParallelQueue(serviceDescriptors, string.Empty); 25 | } 26 | 27 | public static ParallelQueueBuilder AddParallelQueue(this IServiceCollection serviceDescriptors) 28 | { 29 | return AddParallelQueue(serviceDescriptors, string.Empty); 30 | } 31 | 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Extensions/HttpClient/GetClientTokenMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Xfrogcn.AspNetCore.Extensions 7 | { 8 | /// 9 | /// 此类自动向发出的HTTP请求头中添加token 10 | /// 11 | class GetClientTokenMessageHandler : DelegatingHandler 12 | { 13 | public const string IGNORE_TOKEN_PROPERTY = nameof(IGNORE_TOKEN_PROPERTY); 14 | private readonly ClientCertificateManager _tokenManager; 15 | public GetClientTokenMessageHandler(ClientCertificateManager tokenManager) 16 | { 17 | _tokenManager = tokenManager; 18 | } 19 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 20 | { 21 | // 忽略token 22 | if (request.Properties.ContainsKey(IGNORE_TOKEN_PROPERTY)) 23 | { 24 | return await base.SendAsync(request, cancellationToken); 25 | } 26 | HttpResponseMessage lastResponse = null; 27 | try 28 | { 29 | var response = await _tokenManager.Execute(async (token, setter, checker) => 30 | { 31 | await setter.SetTokenAsync(request, token); 32 | var response = await base.SendAsync(request, cancellationToken); 33 | lastResponse = response; 34 | await checker.CheckResponseAsync(response); 35 | return response; 36 | }); 37 | } 38 | catch 39 | { 40 | 41 | } 42 | 43 | return lastResponse; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/Http/RequestLog/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Testing; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using System.Net.Http; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace RequestLog 9 | { 10 | class Program 11 | { 12 | static async Task Main(string[] args) 13 | { 14 | // 集成测试服务端 15 | TestWebApplicationFactory factory = new TestWebApplicationFactory(); 16 | 17 | 18 | IServiceCollection sc = new ServiceCollection() 19 | .AddExtensions(null, config => 20 | { 21 | // 同时设置EnableClientRequestLog为true及ClientRequestLevel为Verbose,可记录客户端请求详情 22 | config.EnableClientRequestLog = true; 23 | config.ClientRequestLevel = Serilog.Events.LogEventLevel.Verbose; 24 | config.FileLog = false; 25 | config.ConsoleLog = true; 26 | }); 27 | 28 | // 此处配置HttpClient使用集成测试服务端的消息处理器 29 | // 注意,此示例不使用factory.CreateClient来创建客户端,因为此方法不是通过HttpFactory方式创建的 30 | // 故无法注入扩展功能 31 | sc.AddHttpClient("", client=> 32 | { 33 | client.BaseAddress = new Uri("http://localhost"); 34 | }) 35 | .ConfigurePrimaryHttpMessageHandler(() => 36 | { 37 | return factory.Server.CreateHandler(); 38 | }); 39 | IServiceProvider sp = sc.BuildServiceProvider(); 40 | 41 | IHttpClientFactory httpFactory = sp.GetRequiredService(); 42 | var client = httpFactory.CreateClient(""); 43 | var response =await client.GetAsync>("/WeatherForecast"); 44 | 45 | 46 | Console.ReadLine(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/MockHttpRequest/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | 6 | namespace MockHttpRequest 7 | { 8 | class Program 9 | { 10 | static async Task Main(string[] args) 11 | { 12 | IServiceCollection sc = new ServiceCollection() 13 | .AddExtensions(); 14 | 15 | sc.AddHttpClient("", client => 16 | { 17 | client.BaseAddress = new Uri("http://localhost"); 18 | }) 19 | .AddMockHttpMessageHandler() 20 | // 请求/api/test1 返回Hello, url可以使用*,便是所有url 21 | .AddMock("/api/test1", HttpMethod.Get, "Hello") 22 | // 通过请求判断 23 | .AddMock(request => 24 | { 25 | // 包含x-test头时,Mock 26 | if (request.Headers.GetValues("x-test") != null) 27 | { 28 | return true; 29 | } 30 | return false; 31 | }, async (request, response) => 32 | { 33 | await response.WriteObjectAsync("TEST"); 34 | }); 35 | 36 | var sp = sc.BuildServiceProvider(); 37 | 38 | var httpFactory = sp.GetRequiredService(); 39 | // 注意名称需要与配置时匹配,此处为"" 40 | var client = httpFactory.CreateClient(""); 41 | 42 | string response = await client.GetAsync("/api/test1"); 43 | Console.WriteLine(response); 44 | 45 | response = await client.GetAsync("api/test2", null, new System.Collections.Specialized.NameValueCollection() 46 | { 47 | {"x-test", "" } 48 | }); 49 | Console.WriteLine(response); 50 | 51 | Console.ReadLine(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/QueueHandlers/QueueHandlerContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http.Headers; 4 | using System.Text; 5 | 6 | namespace Xfrogcn.AspNetCore.Extensions 7 | { 8 | public class QueueHandlerContext 9 | { 10 | public TEntity Message { get; internal set; } 11 | 12 | public TState State { get; internal set; } 13 | 14 | public string QueueName { get; internal set; } 15 | 16 | public IServiceProvider ServiceProvider { get; internal set; } 17 | 18 | public bool Stoped { get; internal set; } = false; 19 | 20 | private Dictionary _parameters = new Dictionary(); 21 | 22 | public IReadOnlyDictionary Parameters => _parameters; 23 | 24 | public void SetParameter(string name, object val) 25 | { 26 | if (_parameters.ContainsKey(name)) 27 | { 28 | _parameters[name] = val; 29 | } 30 | else 31 | { 32 | _parameters.Add(name, val); 33 | } 34 | } 35 | 36 | public void RemoveParameter(string name) 37 | { 38 | if (_parameters.ContainsKey(name)) 39 | { 40 | _parameters.Remove(name); 41 | } 42 | } 43 | 44 | public object GetParameter(string name) 45 | { 46 | if (_parameters.ContainsKey(name)) 47 | { 48 | return _parameters[name]; 49 | } 50 | return default; 51 | } 52 | 53 | public void ClearParamater() 54 | { 55 | _parameters.Clear(); 56 | } 57 | 58 | public TValue GetParameter(string name) 59 | { 60 | return (TValue)GetParameter(name); 61 | } 62 | 63 | public void Stop() 64 | { 65 | Stoped = true; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/Http/ApiServer/BasicAuthenticationHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Options; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net.Http.Headers; 8 | using System.Security.Claims; 9 | using System.Text; 10 | using System.Text.Encodings.Web; 11 | using System.Threading.Tasks; 12 | 13 | namespace ApiServer 14 | { 15 | public class BasicAuthenticationHandler : AuthenticationHandler 16 | { 17 | public BasicAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) 18 | { 19 | } 20 | 21 | protected async override Task HandleAuthenticateAsync() 22 | { 23 | var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]); 24 | var credentialBytes = Convert.FromBase64String(authHeader.Parameter); 25 | var credentials = Encoding.UTF8.GetString(credentialBytes).Split(new[] { ':' }, 2); 26 | var username = credentials[0]; 27 | var password = credentials[1]; 28 | 29 | if(!(username.Equals("test", StringComparison.OrdinalIgnoreCase) && password == "test")) 30 | { 31 | return AuthenticateResult.Fail("Invalid Username or Password"); 32 | } 33 | 34 | var claims = new[] { 35 | new Claim(ClaimTypes.NameIdentifier, username), 36 | new Claim(ClaimTypes.Name, username), 37 | }; 38 | 39 | var identity = new ClaimsIdentity(claims, Scheme.Name); 40 | var principal = new ClaimsPrincipal(identity); 41 | var ticket = new AuthenticationTicket(principal, Scheme.Name); 42 | 43 | return AuthenticateResult.Success(ticket); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /doc/MockHttpReuest.md: -------------------------------------------------------------------------------- 1 | # 请求模拟 2 | 3 | 在微服务架构下,服务之间相互调用,这对测试和调试带来了挑战,请求模拟功能,可以让你专注于你自己的服务,屏蔽外部服务的影响。可灵活配置依赖服务的应答来对本地服务进行测试或调试。 4 | 5 | 请求模拟功能与`IHttpClientFactory`模式结合,可为指定名称的HttpClient提供模拟应答的配置。 6 | 7 | ## 示例 8 | 9 | 以下示例针对于默认名称("")的HttpClient配置了两条规则: 10 | 11 | - Url为"/api/test1"的Get请求:返回字符串"Hello" 12 | - 对于请求头中包含"x-test"头的请求:返回字符串"TEST" 13 | 14 | ```c# 15 | class Program 16 | { 17 | static async Task Main(string[] args) 18 | { 19 | IServiceCollection sc = new ServiceCollection() 20 | .AddExtensions(); 21 | 22 | sc.AddHttpClient("", client => 23 | { 24 | client.BaseAddress = new Uri("http://localhost"); 25 | }) 26 | .AddMockHttpMessageHandler() 27 | // 请求/api/test1 返回Hello, url可以使用*,便是所有url 28 | .AddMock("/api/test1", HttpMethod.Get, "Hello") 29 | // 通过请求判断 30 | .AddMock(request => 31 | { 32 | // 包含x-test头时,Mock 33 | if (request.Headers.GetValues("x-test") != null) 34 | { 35 | return true; 36 | } 37 | return false; 38 | }, async (request, response) => 39 | { 40 | await response.WriteObjectAsync("TEST"); 41 | }); 42 | 43 | var sp = sc.BuildServiceProvider(); 44 | 45 | var httpFactory = sp.GetRequiredService(); 46 | // 注意名称需要与配置时匹配,此处为"" 47 | var client = httpFactory.CreateClient(""); 48 | 49 | string response = await client.GetAsync("/api/test1"); 50 | Console.WriteLine(response); 51 | 52 | response = await client.GetAsync("api/test2", null, new System.Collections.Specialized.NameValueCollection() 53 | { 54 | {"x-test", "" } 55 | }); 56 | Console.WriteLine(response); 57 | 58 | Console.ReadLine(); 59 | } 60 | } 61 | ``` 62 | 63 | 详细示例请参考:examples/Http/MockHttpRequest项目 -------------------------------------------------------------------------------- /src/Extensions/TrackingHeaders.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace Xfrogcn.AspNetCore.Extensions 7 | { 8 | public class TrackingHeaders : ICollection 9 | { 10 | private readonly Dictionary _dic = new Dictionary(StringComparer.OrdinalIgnoreCase); 11 | 12 | public int Count => _dic.Count; 13 | 14 | public bool IsReadOnly => false; 15 | 16 | public void Add(string item) 17 | { 18 | if (string.IsNullOrWhiteSpace(item)) 19 | { 20 | return; 21 | } 22 | lock (_dic) 23 | { 24 | _dic[item] = new Regex($"^{item.Replace("*", ".*")}$", RegexOptions.IgnoreCase); 25 | } 26 | } 27 | 28 | public IEnumerable HeaderRegex => _dic.Values; 29 | 30 | public void Clear() 31 | { 32 | lock (_dic) 33 | { 34 | _dic.Clear(); 35 | } 36 | } 37 | 38 | public bool Contains(string item) 39 | { 40 | return _dic.ContainsKey(item); 41 | } 42 | 43 | public void CopyTo(string[] array, int arrayIndex) 44 | { 45 | _dic.Keys.CopyTo(array, arrayIndex); 46 | } 47 | 48 | public IEnumerator GetEnumerator() 49 | { 50 | return _dic.Keys.GetEnumerator(); 51 | } 52 | 53 | public bool Remove(string item) 54 | { 55 | bool isok = false; 56 | lock (_dic) 57 | { 58 | if (_dic.ContainsKey(item)) 59 | { 60 | isok = true; 61 | _dic.Remove(item); 62 | } 63 | } 64 | return isok; 65 | } 66 | 67 | IEnumerator IEnumerable.GetEnumerator() 68 | { 69 | return _dic.Keys.GetEnumerator(); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/Http/HttpHeaderTracking/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | 6 | namespace HttpHeaderTracking 7 | { 8 | class Program 9 | { 10 | static async Task Main(string[] args) 11 | { 12 | // 以下模拟 客户端-->服务A-->服务B 的调用链路,客户端传入头x-request-id,将自动传递到服务B 13 | // 集成测试服务端 14 | ServiceBFactory factoryB = new ServiceBFactory(); 15 | ServiceAFactory factoryA = new ServiceAFactory(factoryB); 16 | 17 | 18 | IServiceCollection sc = new ServiceCollection() 19 | .AddExtensions(null, config => 20 | { 21 | // 同时设置EnableClientRequestLog为true及ClientRequestLevel为Verbose,可记录客户端请求详情 22 | config.FileLog = false; 23 | config.ConsoleLog = true; 24 | }); 25 | 26 | // 此处配置HttpClient使用集成测试服务端的消息处理器 27 | // 注意,此示例不使用factory.CreateClient来创建客户端,因为此方法不是通过HttpFactory方式创建的 28 | // 故无法注入扩展功能 29 | sc.AddHttpClient("", client => 30 | { 31 | client.BaseAddress = new Uri("http://localhost"); 32 | }) 33 | .ConfigurePrimaryHttpMessageHandler(() => 34 | { 35 | return factoryA.Server.CreateHandler(); 36 | }); 37 | IServiceProvider sp = sc.BuildServiceProvider(); 38 | 39 | IHttpClientFactory httpFactory = sp.GetRequiredService(); 40 | var client = httpFactory.CreateClient(""); 41 | // 调用服务A 42 | var response = await client.GetAsync("/tracking/serviceA", null, new System.Collections.Specialized.NameValueCollection() 43 | { 44 | { "x-request-id", "test-request-id" } 45 | }); 46 | 47 | Console.WriteLine(response); 48 | 49 | Console.ReadLine(); 50 | 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Extensions/AutoMapper/CircularRefChecker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Reflection; 6 | using System.Text; 7 | 8 | namespace Xfrogcn.AspNetCore.Extensions.AutoMapper 9 | { 10 | public class CircularRefChecker 11 | { 12 | internal static MethodInfo GetValueMethod = typeof(CircularRefChecker).GetMethod(nameof(GetValue), BindingFlags.Public | BindingFlags.Instance); 13 | internal static MethodInfo SetInstanceMethod = typeof(CircularRefChecker).GetMethod(nameof(SetInstance), BindingFlags.Public | BindingFlags.Instance); 14 | 15 | class RefKey 16 | { 17 | public object Instance { get; set; } 18 | 19 | public Type TargetType { get; set; } 20 | 21 | public RefKey(object instance, Type targetType) 22 | { 23 | Instance = instance; 24 | TargetType = targetType; 25 | } 26 | 27 | public override bool Equals(object obj) 28 | { 29 | if( obj == null || !(obj is RefKey)) 30 | { 31 | return false; 32 | } 33 | 34 | var other = obj as RefKey; 35 | 36 | return Instance == other.Instance && TargetType == other.TargetType; 37 | } 38 | 39 | public override int GetHashCode() 40 | { 41 | return (Instance == null ? 0 : Instance.GetHashCode()) ^ (TargetType == null ? 0 : TargetType.GetHashCode()); 42 | } 43 | } 44 | readonly Hashtable _cache = new Hashtable(); 45 | 46 | public object GetValue(object source, Type targetType) 47 | { 48 | RefKey key = new RefKey(source, targetType); 49 | return _cache[key]; 50 | } 51 | 52 | public void SetInstance(object source, Type targetType, object targetInstance) 53 | { 54 | RefKey key = new RefKey(source, targetType); 55 | _cache[key] = targetInstance; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASP.NET Core 扩展库 2 | 3 | ASP.NET Core扩展库是针对.NET Core常用功能的扩展,包含日志、Token提供器、并行队列处理、HttpClient扩展、轻量级的DTO类型映射等功能。 4 | 5 | ## 日志扩展 6 | 7 | 扩展库中,我们对Serilog日志库进行了简单的封装使其更加容易配置,同时也增强了本地文件日志Sink,使其支持更复杂的日志目录结构。 8 | 9 | 有关日志的详细说明,请参考[日志](./doc/Logging.md)文档 10 | 11 | ## 轻量级实体映射 12 | 13 | 在分层设计模式中,各层之间的数据通常通过数据传输对象(DTO)来进行数据的传递,而大多数情况下,各层数据的定义结构大同小异,如何在这些定义结构中相互转换,之前我们通过使用[AutoMapper](http://automapper.org/)库,但**AutoMapper**功能庞大,在很多场景下,可能我们只需要一些基础功能,那么此时你可以选择扩展库中的轻量级AutoMapper实现。 14 | 15 | 核心功能 16 | 17 | - 在使用之前无需手动定义类型之间的映射关系 18 | - 采用动态编译、缓存转换委托,提升性能。 19 | - 支持通过特性定义属性映射关系 20 | - 支持插入自定义的转换处理方法 21 | - 支持列表转换 22 | - 支持嵌套类型转换 23 | - 支持循环引用及引用关系维持 24 | - 支持转换模式或拷贝模式 25 | - 支持生成预定义的拷贝委托 26 | - 为了保持其轻量性,目前支持以下转换 27 | - 值类型转换 28 | - 数值类型之间的兼容转换(如int-->uint) 29 | - 支持值类型与其可空类型间的兼容转换 30 | - 字典类型转换 31 | - 列表类型转换 32 | - 枚举类型与string类型间的转换 33 | - **不支持**结构体之间的转换以及结构体与类之间的转换 34 | 35 | 有关轻量级实体映射的详细说明,请参考[轻量级实体映射](./doc/LightweightMapper.md)文档 36 | 37 | ## AspNetCore Http服务端的扩展 38 | 39 | 针对AspNetCore Http服务端,扩展库提供了以下功能: 40 | 41 | - 请求与应答详细日志记录 42 | - EnableBufferingAttribute特性,开启请求的Buffer(可重复读取) 43 | 44 | 详细信息请参考[Http服务端扩展](./doc/HttpServer.md) 45 | 46 | ## HttpClient扩展 47 | 48 | .NET Core扩展库中通过HttpFactory及HttpClient来执行HTTP请求调用,HttpClient扩展在此基础上进行了更多功能的扩展,增加易用性、可测试性。 49 | 50 | HttpClient包含以下功能: 51 | 52 | - 针对HttpClient的相关扩展方法,请参考[此文档](./doc/HttpClient.md) 53 | - 针对HttpRequestMessage及HttpResponseMessage的扩展方法,请参考[此文档](./doc/HttpMessageExtensions.md) 54 | - 请求日志记录 55 | - 请求头的自动传递(请求链路跟踪),请参考[此文档](./doc/RequestTracing.md) 56 | - Http请求模拟(用于测试或模拟第三方服务),请参考[此文档](./doc/MockHttpReuest.md) 57 | - Http受限请求中,可自动获取及管理访问令牌,请参考[此文档](./doc/TokenRequest.md) 58 | 59 | ## 令牌提供器 60 | 61 | 令牌提供器用于管理应用的相关访问令牌,主要由HttpClient扩展使用。当然你也可以单独使用。 62 | 63 | 令牌提供器包含以下功能: 64 | 65 | - 认证客户端的管理 66 | - 访问令牌的缓存(支持本地缓存及分布式缓存) 67 | - 访问令牌的自动获取及自动过期处理 68 | - 访问令牌的获取器(即如何从认证服务中获取指定客户端的访问令牌) 69 | - 访问令牌的设置器(即如何在Http请求中设置访问令牌) 70 | - 访问令牌的失效判断器(即如何判断请求令牌失效) 71 | 72 | 有关令牌提供器的详细说明,请参考[此文档](./doc/TokenProvider.md) 73 | 74 | ## 并行队列处理 75 | 76 | 并行队列处理可以将一个大的队列,拆分到多个子队列进行并行处理,以提高处理效率。同时,在每个子队列处理中实现了处理管道,可灵活扩展。 77 | ![Img](并行队列.png) 78 | 79 | 有关并行队列的文档,请参考[并行队列](./doc/ParallelQueue.md) -------------------------------------------------------------------------------- /examples/Http/BasicAuth/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Testing; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | 7 | namespace BasicAuth 8 | { 9 | class Program 10 | { 11 | static async Task Main(string[] args) 12 | { 13 | // 使用Basic认证方式,通过配置后,发送的请求自动处理请求认证信息,应用无需单独处理 14 | // 以下示例/limit 路径受限,需要通过Basic认证(用户名:test,密码: test),接口返回用户名 15 | WebApplicationFactory server = new WebApplicationFactory(); 16 | 17 | IServiceCollection sc = new ServiceCollection() 18 | .AddExtensions(null, config => 19 | { 20 | config.FileLog = false; 21 | config.ConsoleLog = true; 22 | }); 23 | 24 | // 1. 首先,需要加入客户端信息,每个客户端必须有唯一的ID 25 | sc.AddClientTokenProvider(options => 26 | { 27 | options.AddClient("", "TestClient", "") 28 | // 设置此客户端使用Basic认证 29 | .UseBasicAuth("test", "test"); 30 | }); 31 | 32 | // 此处配置HttpClient使用集成测试服务端的消息处理器 33 | // 注意,此示例不使用factory.CreateClient来创建客户端,因为此方法不是通过HttpFactory方式创建的 34 | // 故无法注入扩展功能 35 | sc.AddHttpClient("", client => 36 | { 37 | client.BaseAddress = new Uri("http://localhost"); 38 | }) 39 | .ConfigurePrimaryHttpMessageHandler(() => 40 | { 41 | return server.Server.CreateHandler(); 42 | }) 43 | // 设置此客户端关联TestClient的认证配置 44 | .AddTokenMessageHandler("TestClient"); 45 | IServiceProvider sp = sc.BuildServiceProvider(); 46 | 47 | IHttpClientFactory httpFactory = sp.GetRequiredService(); 48 | var client = httpFactory.CreateClient(""); 49 | string response = await client.GetAsync("/limit"); 50 | 51 | Console.WriteLine(response); 52 | 53 | Console.ReadLine(); 54 | 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/Extensions.Tests/WebApiHostExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.Extensions.Logging; 8 | using Serilog.Context; 9 | using Xunit; 10 | 11 | namespace Extensions.Tests 12 | { 13 | [Trait("", "WebApiHost")] 14 | public class WebApiHostExtensionsTest 15 | { 16 | [Fact(DisplayName = "Log")] 17 | public void Test1() 18 | { 19 | var host = WebHost.CreateDefaultBuilder() 20 | .UseExtensions(null) 21 | .ConfigureLogging(logBuilder => 22 | { 23 | logBuilder.AddTestLogger(); 24 | }) 25 | .UseStartup() 26 | .Build(); 27 | 28 | var logger = host.Services.GetRequiredService>(); 29 | int count = 10; 30 | using (LogContext.PushProperty("aaa", "BBB")) 31 | { 32 | string log = new string('a', 1024 * 1024); 33 | 34 | using (var scope = logger.BeginScope("this is a {scope}", "scope")) 35 | { 36 | for (int i = 0; i < count; i++) 37 | { 38 | logger.LogInformation(log); 39 | } 40 | } 41 | } 42 | 43 | var logContent = host.Services.GetTestLogContent(); 44 | Assert.Equal(count, logContent.LogContents.Count); 45 | } 46 | } 47 | 48 | public class Startup 49 | { 50 | public Startup(IConfiguration configuration) 51 | { 52 | Configuration = configuration; 53 | } 54 | 55 | public IConfiguration Configuration { get; } 56 | 57 | public void ConfigureServices(IServiceCollection services) 58 | { 59 | 60 | 61 | } 62 | 63 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 64 | { 65 | 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Extensions/HttpClient/MessageHandlerFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.Extensions.Http; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Extensions.Options; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace Xfrogcn.AspNetCore.Extensions 9 | { 10 | public class MessageHandlerFilter : IHttpMessageHandlerBuilderFilter 11 | { 12 | private readonly ILoggerFactory _loggerFactory = null; 13 | private readonly IHttpContextAccessor _httpContextAccessor; 14 | private readonly IServiceProvider _provider; 15 | 16 | 17 | public MessageHandlerFilter( 18 | ILoggerFactory loggerFactory, 19 | IServiceProvider serviceProvider, 20 | IHttpContextAccessor httpContextAccessor) 21 | { 22 | _loggerFactory = loggerFactory; 23 | _provider = serviceProvider; 24 | _httpContextAccessor = httpContextAccessor; 25 | } 26 | 27 | 28 | 29 | public MessageHandlerFilter(ILoggerFactory loggerFactory, IServiceProvider serviceProvider) : 30 | this(loggerFactory, serviceProvider, (IHttpContextAccessor)null) 31 | { 32 | } 33 | 34 | public Action Configure(Action next) 35 | { 36 | return (builder) => 37 | { 38 | // 先调用其他的配置 39 | next(builder); 40 | 41 | WebApiConfig cfg = _provider.GetRequiredService(); 42 | 43 | // 将自定义的LoggingDetailMessageHandler放到首位 44 | if( cfg!=null && cfg.EnableClientRequestLog) 45 | { 46 | ILogger requestLogger = _loggerFactory.CreateLogger("ClientRequest.Logger"); 47 | builder.AdditionalHandlers.Insert(0, new LoggingDetailMessageHandler(requestLogger)); 48 | } 49 | 50 | if (_httpContextAccessor != null) 51 | { 52 | builder.AdditionalHandlers.Insert(0, new TransRequestHeadersMessageHandler(_httpContextAccessor, cfg)); 53 | } 54 | }; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/Logging/JsonFileLogging/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | using System; 5 | using System.Drawing; 6 | using Xfrogcn.AspNetCore.Extensions; 7 | 8 | namespace JsonFileLogging 9 | { 10 | class Program 11 | { 12 | static void Main(string[] args) 13 | { 14 | using IHost host = Host.CreateDefaultBuilder() 15 | .UseExtensions(config => 16 | { 17 | config.FileJsonLog = true; 18 | config.AppLogLevel = Serilog.Events.LogEventLevel.Verbose; 19 | 20 | // 指定日志存放目录 21 | // config.LogPath = "Logs"; 22 | //指定日志文件名称模板,默认为日期为目录,日志名称为文件名称 23 | //config.LogPathTemplate = LogPathTemplates.DayFolderAndLevelFile; 24 | // 通过EnableSerilog来设置是否使用Serilog日志框架 25 | //config.EnableSerilog = false; 26 | // 设置文件的最大尺寸,超过此尺寸后将产生新的日志序列文件 27 | config.MaxLogFileSize = 1024; 28 | // 单个日志文件拆分的最大数量,超过此数量后日志将被自动压缩 29 | // config.RetainedFileCount = 1; 30 | // 日志最长保留天数,超过此设置后的日志将被自动清理 31 | //config.MaxLogDays = 7; 32 | // 单条日志内容的最大长度,超过后将被拆分或忽略超长部分(取决于IgnoreLongLog) 33 | // config.MaxLogLength = 1024 * 8; 34 | }) 35 | .Build(); 36 | 37 | _ = host.StartAsync(); 38 | 39 | var logger = host.Services.GetRequiredService>(); 40 | logger.LogInformation("测试日志:{time}", DateTimeOffset.Now); 41 | // 使用@前缀将对象合并大日志JSON对象中,如果不带@前缀,则以字符串方式进行处理 42 | logger.LogInformation("obj: {@obj}", new Point { X = 0, Y = 1 }); 43 | for(int i = 0; i < 10; i++) 44 | { 45 | logger.LogInformation(new string('A', 1024)); 46 | } 47 | 48 | Console.ReadLine(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Extensions/Xfrogcn.AspNetCore.Extensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | true 6 | Xfrogcn.AspNetCore.Extensions 7 | 王海波 8 | 王海波 9 | true 10 | AspNetCore extensions library,includes function as logging, HTTP requests, authentication token management, parallel queue processing, and more 11 | AspNetCore extensions library,includes function as logging, HTTP requests, authentication token management, parallel queue processing, and more 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Extensions/TokenProvider/CheckResponseProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | 5 | namespace Xfrogcn.AspNetCore.Extensions 6 | { 7 | /// 8 | /// 应答检查,判断是否令牌失效 9 | /// 10 | public abstract class CheckResponseProcessor 11 | { 12 | /// 13 | /// 检查应答,如果令牌无效,触发UnauthorizedAccessException异常 14 | /// 15 | /// 16 | /// 17 | public abstract Task CheckResponseAsync(HttpResponseMessage response); 18 | 19 | 20 | public static CheckResponseProcessor NormalChecker = new NormalResponseProcessor(); 21 | 22 | 23 | public static DelegateCheckResponseProcessor CreateDelegateCheckResponseProcessor(Func checker) 24 | { 25 | return new DelegateCheckResponseProcessor(checker); 26 | } 27 | 28 | 29 | /// 30 | /// 通过查询字符串传递访问令牌 31 | /// 32 | public class NormalResponseProcessor : CheckResponseProcessor 33 | { 34 | 35 | public NormalResponseProcessor() 36 | { 37 | 38 | } 39 | 40 | public override Task CheckResponseAsync(HttpResponseMessage response) 41 | { 42 | if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || 43 | response.StatusCode == System.Net.HttpStatusCode.Forbidden) 44 | { 45 | throw new UnauthorizedAccessException("验证失败"); 46 | } 47 | return Task.CompletedTask; 48 | } 49 | } 50 | 51 | public class DelegateCheckResponseProcessor : CheckResponseProcessor 52 | { 53 | private readonly Func _checker; 54 | public DelegateCheckResponseProcessor(Func checker) 55 | { 56 | _checker = checker; 57 | } 58 | 59 | public override Task CheckResponseAsync(HttpResponseMessage response) 60 | { 61 | if (_checker != null) 62 | { 63 | return _checker(response); 64 | } 65 | return Task.CompletedTask; 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /doc/Logging.md: -------------------------------------------------------------------------------- 1 | # 日志 2 | 3 | Xfrogcn.AspNetCore.Extensions中日志是对Serilog日志库易用性的进一步封装,通过简单配置来实现常见的日志记录需求。 4 | 5 | 日志通过全局的WebApiConfig进行配置。 6 | 7 | ## 日志配置项 8 | 9 | | 配置项 | 说明 | 默认值 | 10 | | ----- | -------------- | ---- | 11 | | EnableSerilog | 是否启用Serilog | true | 12 | | SystemLogLevel | 系统日志级别, 系统日志是指日志源以Microsoft或System开始的日志 | Warning | 13 | | EFCoreCommandLevel | EFCore日志级别,EFCore日志是指日志来源为Microsoft.EntityFrameworkCore.Database.Command的日志 | Information | 14 | | AppLogLevel | 应用日志级别,应用日志指来源为Microsoft.AspNetCore、Microsoft.Hosting及其他来源的日志 | Information | 15 | | ServerRequestLevel | 服务端请求记录日志级别,设置为Verbose可记录服务端请求及应答详情 | Information | 16 | | ClientRequestLevel | 客户端请求记录日志级别 | Information | 17 | | ConsoleLog | 是否开启控制台日志 | false | 18 | | ConsoleJsonLog | 控制台日志是否使用Json日志格式 | false | 19 | | FileLog | 是否开启本地文件日志 | true | 20 | | FileJsonLog | 本地文件日志是否使用Json日志格式 | false | 21 | | MaxLogLength | 单条日志的最大长度 | 8kb | 22 | | LogPathTemplate | 日志路径模板 | LogPathTemplates.DayFolderAndLoggerNameFile | LogPath | 文件日志保存位置 | "Logs" | 23 | | MaxLogFileSize | 最大单个日志文件大小,超过后将写到新的日志 | 100mb | 24 | | RetainedFileCount | 旋转日志的文件数,超过此设置将被自动压缩 | 31 | 25 | | LogTemplate | 日志模板 | "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{NewLine}{Exception}" | 26 | | MaxLogDays | 本地文件日志最大保留天数, 如果设置为0,不做自动清理 | 0 | 27 | | IgnoreLongLog | 是否忽略长日志(即超过MaxLogLength设置的日志) | false | 28 | 29 | ## 如何使用 30 | 31 | ```c# 32 | using IHost host = Host.CreateDefaultBuilder() 33 | .UseExtensions(config => 34 | { 35 | // 在此处配置日志选项 36 | // config.FileLog = true; 37 | config.AppLogLevel = Serilog.Events.LogEventLevel.Verbose; 38 | }) 39 | .Build(); 40 | ``` 41 | 42 | ## 关于长日志 43 | 44 | 对长日志的控制,仅针对于Json格式,通过MaxLogLength及IgnoreLongLog配置来设置对长日志的处理,正常情况,超过MaxLogLength设置的日志将被截取到MaxLogLength长度,但如果IgnoreLongLog设置为false,则超过MaxLogLength长度的日志将同时进行拆分记录为多条日志。 45 | 46 | 非Json格式日志不受长度设置影响。 47 | 48 | ## 关于本地文件日志记录路径 49 | 50 | 本地文件日志路径通过LogPathTemplate设置来配置,默认为LogPathTemplates.DayFolderAndLoggerNameFile,表示以每天作为子目录,以日志名称作为日志文件名。通过LogPathTemplates也内置了其他的路径模板: 51 | 52 | | 路径模板名 | 说明 | 53 | | ---------- | ---- | 54 | | DayFolderAndLoggerNameFile | 以每天日期为目录,日志名称为文件名 | 55 | | DayFile | 以每天日期为日志名称 | 56 | | LoggerNameAndDayFile | 以[日志名称_每天日志]为日志文件名称 | 57 | | LevelFile | 以日志级别缩写为日志文件名称 | 58 | | DayFolderAndLevelFile | 以每天日期为目录,日志级别缩写为日志名称 | 59 | 60 | 由于LogPathTemplate未字符串配置,你也可以配置其他的路径模板。 61 | -------------------------------------------------------------------------------- /doc/ParallelQueue.md: -------------------------------------------------------------------------------- 1 | # 并行队列 2 | 3 | 并行队列处理可以将一个大的队列,拆分到多个子队列进行并行处理,以提高处理效率。同时,在每个子队列处理中实现了处理管道,可灵活扩展。 4 | ![Img](../并行队列.png) 5 | 6 | - 生产者队列:代表原始队列,如Redis、MQ等 7 | - 消费者队列:从生产者队列中获取消息,作为待处理队列 8 | - 队列处理器:处理单个消息的处理管道 9 | 10 | ## 生产者队列 11 | 12 | 生产者队列代码原始队列,扩展库提供本地内存队列(用于测试)以及Redis队列(通过Xfrogcn.AspNetCore.Extensions.RedisQueueProducer库提供),当然,你也可是实现自己的生产者队列。 13 | 14 | 你可以通过ParallelQueueBuilder的ConfigProducer方法来配置生产者队列: 15 | 16 | ```c# 17 | // 添加一个名称为TEST_QUEUE的并行处理队列 18 | services.AddParallelQueue(QUEUE_NAME) 19 | // 配置生产者队列 20 | .ConfigProducer(options => 21 | { 22 | // 使用Redis队列 23 | options.UseRedis(redisOptions=>{ 24 | // redis配置 25 | }); 26 | // 或者使用内存队列 27 | // options.UseMemory(); 28 | }) 29 | ``` 30 | 31 | ## 队列处理器 32 | 33 | 队列消息的处理采用管道模式,你可以在管道中添加自己的消息处理器,消息处理器从QueueHandlerBase<TMessage>继承,其中Order属性用于指定执行顺序,处理器按照Order升序方式排列执行。 34 | 35 | ```c# 36 | /// 37 | /// 消息处理器,从QueueHandlerBase继承,实现对单个消息的处理 38 | /// 39 | class MessageHandlerA : QueueHandlerBase 40 | { 41 | /// 42 | /// 顺序,越小的排在前面 43 | /// 44 | public override int Order => base.Order; 45 | 46 | public override Task Process(QueueHandlerContext context) 47 | { 48 | // 示例: 演示消息处理管道 49 | context.Message.Output = context.Message.Input + 1; 50 | return base.Process(context); 51 | } 52 | } 53 | ``` 54 | 55 | ## 配置 56 | 57 | 要使用并行队列,可通过IServiceCollection上的AddParallelQueue扩展方法进行配置: 58 | 59 | ```c# 60 | const string QUEUE_NAME = "TEST_QUEUE"; 61 | 62 | // 添加一个名称为TEST_QUEUE的并行处理队列 63 | services.AddParallelQueue(QUEUE_NAME) 64 | // 添加对应的默认HostedService,通过该托管服务将关联ParallelQueueProducer和ParallelQueueConsumer 65 | .AddHostedService() 66 | // 配置生产者队列 67 | .ConfigProducer(options => 68 | { 69 | // 使用内存队列 70 | options.UseMemory(); 71 | }) 72 | // 配置使用管道模式的Consumer 73 | .ConfigConsumerHandler(1, 5) 74 | // 加入消息处理器A 75 | .AddConsumerHandler() 76 | // 加入消息处理器B 77 | .AddConsumerHandler(); 78 | ``` 79 | 80 | 有关并行队列的演示示例,请参考`examples/ParallelQueue/ParallelQueueExample`项目 81 | -------------------------------------------------------------------------------- /src/Extensions/HttpClient/TransRequestHeadersMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.Primitives; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Net.Http; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace Xfrogcn.AspNetCore.Extensions 12 | { 13 | /// 14 | /// 将请求来源的请求头传递给发出请求 15 | /// 16 | class TransRequestHeadersMessageHandler : DelegatingHandler 17 | { 18 | private readonly IHttpContextAccessor _httpContextAccessor; 19 | private readonly WebApiConfig _config; 20 | public TransRequestHeadersMessageHandler(IHttpContextAccessor httpContextAccessor, WebApiConfig config) 21 | { 22 | _httpContextAccessor = httpContextAccessor; 23 | _config = config; 24 | } 25 | 26 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 27 | { 28 | if (_httpContextAccessor != null) 29 | { 30 | SetRequest(request); 31 | } 32 | return base.SendAsync(request, cancellationToken); 33 | } 34 | 35 | protected virtual void SetRequest(HttpRequestMessage client) 36 | { 37 | HttpContext httpContext = null; 38 | if (_httpContextAccessor != null) 39 | { 40 | httpContext = _httpContextAccessor.HttpContext; 41 | } 42 | 43 | if (httpContext == null || httpContext.Request == null || httpContext.Request.Headers == null || client == null) 44 | return; 45 | 46 | IHeaderDictionary header = httpContext.Request.Headers; 47 | var incomingHeaders = _config?.TrackingHeaders?.HeaderRegex; 48 | if (incomingHeaders == null) 49 | return; 50 | 51 | foreach (string key in header.Keys) 52 | { 53 | if (incomingHeaders.Any(x=>x.IsMatch(key))) 54 | { 55 | StringValues sv = header[key]; 56 | 57 | // 当且仅当当前请求头中不存在对应头时才添加 58 | if(client.Headers.Contains(key)) 59 | { 60 | continue; 61 | } 62 | 63 | client.Headers.Add(key, sv.ToString()); 64 | 65 | } 66 | } 67 | } 68 | 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection.Extensions; 6 | using Serilog; 7 | using Xfrogcn.AspNetCore.Extensions; 8 | 9 | namespace Microsoft.Extensions.DependencyInjection 10 | { 11 | public static class ServiceCollectionExtensions 12 | { 13 | internal static WebApiConfig config = new WebApiConfig(); 14 | 15 | internal static Action _configAction = null; 16 | 17 | 18 | public static IServiceCollection AddExtensions(this IServiceCollection serviceDescriptors, IConfiguration configuration = null, Action configAction = null, Action configureLogger = null) 19 | { 20 | _configAction = configAction; 21 | 22 | if (configuration != null) 23 | { 24 | serviceDescriptors.Configure(configuration); 25 | configuration.Bind(config); 26 | } 27 | configAction?.Invoke(config); 28 | 29 | if (config.EnableSerilog) 30 | { 31 | serviceDescriptors.AddDefaultSerilog(config, (logConfig) => 32 | { 33 | configureLogger?.Invoke(logConfig); 34 | if (configuration != null) 35 | { 36 | logConfig.ReadFrom.Configuration(configuration); 37 | } 38 | }); 39 | } 40 | 41 | // 注入消息日志消息处理器 42 | serviceDescriptors.AddHttpClient(); 43 | serviceDescriptors.AddHttpMessageHandlerFilter(); 44 | 45 | serviceDescriptors.AddClientTokenProvider(); 46 | 47 | //实体转换 48 | serviceDescriptors.AddLightweightMapper(); 49 | 50 | serviceDescriptors.TryAddTransient(); 51 | 52 | serviceDescriptors.TryAddTransient(); 53 | serviceDescriptors.TryAddSingleton(); 54 | serviceDescriptors.AddTransient(); 55 | 56 | if (configuration != null) 57 | { 58 | // 默认从配置的_Clients节点获取客户端列表(以客户端名称为key,下配置clientId,clientSecret) 59 | serviceDescriptors.AddClientTokenProvider(configuration); 60 | } 61 | 62 | serviceDescriptors.AddSingleton(config); 63 | serviceDescriptors.AddSingleton(); 64 | 65 | return serviceDescriptors; 66 | } 67 | 68 | 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/AutoMapper/Example/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using System; 4 | using Xfrogcn.AspNetCore.Extensions; 5 | using Xfrogcn.AspNetCore.Extensions.AutoMapper; 6 | 7 | namespace Example 8 | { 9 | class Program 10 | { 11 | static void Main(string[] args) 12 | { 13 | using IHost host = Host.CreateDefaultBuilder() 14 | // UseExtensions会自动注入Mapper 15 | .UseExtensions() 16 | .ConfigureServices(sc => 17 | { 18 | // 通过ConfigureLightweightMapper来配置映射 19 | sc.ConfigureLightweightMapper(options => 20 | { 21 | // 通过AddConvert可自定义转换逻辑 22 | // 以下定义从SourceA转换到TargetB时,自动设置属性C的值 23 | options.AddConvert((mapper, a, b) => 24 | { 25 | b.C = "C"; 26 | }); 27 | }); 28 | }) 29 | .Build(); 30 | 31 | // 你也可以通过AddLightweightMapper单独注入 32 | //IServiceCollection sc = new ServiceCollection() 33 | // .AddLightweightMapper(); 34 | 35 | // 通过IMapperProvider 36 | var mapperProvider = host.Services.GetRequiredService(); 37 | var mapper = mapperProvider.GetMapper(); 38 | var sourceA = new SourceA() 39 | { 40 | A = "A", 41 | B = 1 42 | }; 43 | var targetA = mapper.Convert(sourceA); 44 | 45 | Console.WriteLine($"TargetA: A: {targetA.A} B: {targetA.B}"); 46 | 47 | // 也可以直接获取IMapper<,>实例 48 | var mapperB = host.Services.GetRequiredService>(); 49 | var targetB = mapperB.Convert(sourceA); 50 | Console.WriteLine($"TargetB: A: {targetB.A} B: {targetB.B} C:{targetB.C}"); 51 | 52 | //拷贝 53 | var targetB1 = new TargetB(); 54 | mapperB.CopyTo(sourceA, targetB1); 55 | Console.WriteLine($"TargetB1: A: {targetB1.A} B: {targetB1.B} C:{targetB1.C}"); 56 | 57 | // 只拷贝指定字段之外的属性 58 | var copyProc = mapperB.DefineCopyTo(a => 59 | new 60 | { 61 | a.A //忽略属性A 62 | }); 63 | var targetB2 = new TargetB(); 64 | copyProc(sourceA, targetB2); 65 | Console.WriteLine($"TargetB2: A: {targetB2.A} B: {targetB2.B} C:{targetB2.C}"); 66 | 67 | 68 | Console.ReadLine(); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/ParallelQueue/ParallelQueueExample/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using ParallelQueueExample.Handlers; 4 | using System; 5 | using System.Threading.Tasks; 6 | using Xfrogcn.AspNetCore.Extensions; 7 | 8 | namespace ParallelQueueExample 9 | { 10 | class Program 11 | { 12 | static async Task Main(string[] args) 13 | { 14 | // 以下演示从一个队列接收消息,然后交给5个子队列进行处理,针对每个消息将通过处理管道的两个处理器进行处理 15 | 16 | const string QUEUE_NAME = "TEST_QUEUE"; 17 | IHost host = Host.CreateDefaultBuilder() 18 | .UseExtensions(config=> 19 | { 20 | config.ConsoleLog = true; 21 | config.FileLog = false; 22 | }) 23 | .ConfigureServices(services => 24 | { 25 | // 添加一个名称为TEST_QUEUE的并行处理队列 26 | services.AddParallelQueue(QUEUE_NAME) 27 | // 添加对应的默认HostedService,通过该托管服务将关联ParallelQueueProducer和ParallelQueueConsumer 28 | .AddHostedService() 29 | // 配置生产者队列 30 | .ConfigProducer(options => 31 | { 32 | // 使用内存队列 33 | options.UseMemory(); 34 | }) 35 | // 配置使用管道模式的Consumer 36 | .ConfigConsumerHandler(1, 5) 37 | // 加入消息处理器A 38 | .AddConsumerHandler() 39 | // 加入消息处理器B 40 | .AddConsumerHandler(); 41 | 42 | }) 43 | .Build(); 44 | 45 | await host.StartAsync(); 46 | 47 | // 模拟消息生产 48 | _ = Task.Run(async () => 49 | { 50 | var factory = host.Services.GetRequiredService(); 51 | var producer = factory.CreateProducer(QUEUE_NAME); 52 | int i = 0; 53 | while (true) 54 | { 55 | await producer.TryAddAsync(new NotifyMessage() 56 | { 57 | Input = 0, 58 | Output = 0 59 | }, default); 60 | await Task.Delay(100); 61 | 62 | i++; 63 | if (i > 100) 64 | { 65 | await producer.StopAsync(default); 66 | break; 67 | } 68 | } 69 | }); 70 | 71 | 72 | 73 | Console.ReadLine(); 74 | 75 | await host.StopAsync(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Extensions/Expressions/PredicateBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace Xfrogcn.AspNetCore.Extensions 5 | { 6 | public class PredicateBuilder 7 | { 8 | 9 | private Expression _expression = null; 10 | 11 | private ParameterExpression _parameter; 12 | 13 | public PredicateBuilder() 14 | { 15 | _parameter = Expression.Parameter(typeof(TModel), "m"); 16 | } 17 | 18 | public Expression> Predicate 19 | { 20 | get 21 | { 22 | if(_expression == null) 23 | { 24 | return Expression.Lambda>(Expression.Constant(true), _parameter); ; 25 | } 26 | return Expression.Lambda>(_expression, _parameter); 27 | } 28 | } 29 | 30 | public void And(Expression> predicate) 31 | { 32 | if(predicate == null) 33 | { 34 | return; 35 | } 36 | 37 | var exp = new ParameterExpressionVisitor(predicate.Parameters[0], _parameter).Visit(predicate.Body); 38 | if(_expression == null) 39 | { 40 | _expression = exp; 41 | } 42 | else 43 | { 44 | _expression = Expression.AndAlso( 45 | _expression, 46 | exp 47 | ); 48 | } 49 | } 50 | 51 | 52 | public void Or(Expression> predicate) 53 | { 54 | if (predicate == null) 55 | { 56 | return; 57 | } 58 | 59 | var exp = new ParameterExpressionVisitor(predicate.Parameters[0], _parameter).Visit(predicate.Body); 60 | if (_expression == null) 61 | { 62 | _expression = exp; 63 | } 64 | else 65 | { 66 | _expression = Expression.Or( 67 | _expression, 68 | exp 69 | ); 70 | } 71 | } 72 | 73 | class ParameterExpressionVisitor : ExpressionVisitor 74 | { 75 | private readonly ParameterExpression _source; 76 | private readonly ParameterExpression _target; 77 | public ParameterExpressionVisitor(ParameterExpression source, ParameterExpression target ) 78 | { 79 | _source = source; 80 | _target = target; 81 | } 82 | 83 | protected override Expression VisitParameter(ParameterExpression node) 84 | { 85 | if( node == _source) 86 | { 87 | return _target; 88 | } 89 | return base.VisitParameter(node); 90 | } 91 | } 92 | 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Extensions/IHostBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Serilog; 5 | using Serilog.Core; 6 | using Serilog.Events; 7 | using Xfrogcn.AspNetCore.Extensions; 8 | 9 | namespace Microsoft.Extensions.Hosting 10 | { 11 | public static class IHostBuilderExtensions 12 | { 13 | public static Logger InnerLogger = null; 14 | 15 | 16 | public static IHostBuilder UseExtensions(this IHostBuilder builder, Action configAction = null, Action configureLogger = null) 17 | { 18 | if (builder == null) throw new ArgumentNullException(nameof(builder)); 19 | 20 | AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; 21 | 22 | // 先初始化一个日志,以便在其他配置中可以先行使用 23 | var tempLogger = new LoggerConfiguration() 24 | .MinimumLevel.Is(LogEventLevel.Verbose); 25 | tempLogger = tempLogger.WriteTo.Console(); 26 | var _logger = tempLogger.CreateLogger(); 27 | InnerLogger = _logger; 28 | 29 | 30 | 31 | // 注意 Host的初始化流程 先以此执行 ConfigureAppConfiguration, 最后执行 ConfigureServices 32 | builder = builder.ConfigureServices((context, collection) => 33 | { 34 | collection.AddExtensions(context.Configuration, configAction, configureLogger); 35 | 36 | var config = ServiceCollectionExtensions.config; 37 | StringBuilder sb = new StringBuilder(); 38 | foreach (string h in config.HttpHeaders) 39 | { 40 | if (sb.Length > 0) 41 | { 42 | sb.Append(", "); 43 | } 44 | sb.Append(h); 45 | } 46 | 47 | InnerLogger.Information($"初始化完成:\n" + 48 | $"\t系统日志级别:{config.SystemLogLevel}\n" + 49 | $"\t应用日志级别:{config.AppLogLevel}\n" + 50 | $"\tEF Core Command日志级别:{config.EFCoreCommandLevel}\n" + 51 | $"\t是否开启控制台日志:{config.ConsoleLog}\n" + 52 | $"\t服务端请求日志记录级别:{config.ServerRequestLevel}\n" + 53 | $"\t客户端请求日志级别:{config.ClientRequestLevel}\n" + 54 | $"\t是否开启客户端请求日志:{config.EnableClientRequestLog}\n" + 55 | $"\t日志保留天数:{config.MaxLogDays}\n" + 56 | $"\t记录以下HTTP请求头:{sb.ToString()}"); 57 | }); 58 | 59 | 60 | return builder; 61 | } 62 | 63 | 64 | private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) 65 | { 66 | if (e.ExceptionObject != null && e.ExceptionObject is Exception) 67 | { 68 | Log.Logger.Fatal(e.ExceptionObject as Exception, "系统异常"); 69 | } 70 | else 71 | { 72 | Log.Logger.Fatal($"系统异常"); 73 | } 74 | } 75 | 76 | 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/QueueHandlers/QueueHandlerFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace Xfrogcn.AspNetCore.Extensions 9 | { 10 | public class QueueHandlerFactory 11 | { 12 | readonly List> _handlers = new List>(); 13 | readonly IServiceProvider _serviceProvider; 14 | readonly ILogger> _logger; 15 | readonly IAutoRetry _retry; 16 | 17 | public QueueHandlerFactory( 18 | IServiceProvider serviceProvider, 19 | IAutoRetry retry, 20 | IEnumerable> handlers, 21 | ILogger> logger) 22 | { 23 | if (handlers != null) 24 | { 25 | var list = handlers.ToList().OrderBy(h => h.Order); 26 | _handlers.AddRange(list); 27 | } 28 | _serviceProvider = serviceProvider; 29 | _logger = logger; 30 | _retry = retry; 31 | } 32 | 33 | public virtual async Task Process(TEntity msg, TState state, string name, Func errorHandler ) 34 | { 35 | var list = _handlers.ToList(); 36 | QueueHandlerContext context = new QueueHandlerContext() 37 | { 38 | QueueName = name, 39 | Message = msg, 40 | State = state 41 | }; 42 | foreach(var h in list) 43 | { 44 | try 45 | { 46 | await _retry.Retry(async () => 47 | { 48 | using(var scope = _serviceProvider.CreateScope()) 49 | { 50 | context.ServiceProvider = scope.ServiceProvider; 51 | var handler = scope.ServiceProvider.GetRequiredService(h.GetType()) as IQueueHandler; 52 | await handler.Process(context); 53 | } 54 | 55 | }, 3, 100, true); 56 | } 57 | catch (Exception e) 58 | { 59 | _logger.LogError(e, "处理队列消息失败"); 60 | if (errorHandler != null) 61 | { 62 | using (var scope = _serviceProvider.CreateScope()) 63 | { 64 | await errorHandler(_serviceProvider, e, msg, state, name); 65 | } 66 | } 67 | } 68 | 69 | if (context.Stoped) 70 | { 71 | break; 72 | } 73 | 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Extensions/TokenProvider/CertificateProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Http; 6 | 7 | namespace Xfrogcn.AspNetCore.Extensions 8 | { 9 | /// 10 | /// 获取Token 11 | /// 12 | public abstract class CertificateProcessor 13 | { 14 | public const string HTTP_CLIENT_NAME = nameof(ClientCertificateManager); 15 | 16 | public abstract Task GetToken(ClientCertificateInfo clientInfo, IHttpClientFactory clientFactory); 17 | 18 | public static readonly CertificateProcessor OIDC = new OIDCCertificateProcessor(); 19 | 20 | public static CertificateProcessor CreateDelegateProcessor(Func> proc) 21 | { 22 | return new DelegateCertificateProcessor(proc); 23 | } 24 | 25 | 26 | public class OIDCCertificateProcessor : CertificateProcessor 27 | { 28 | 29 | public override async Task GetToken(ClientCertificateInfo clientInfo, IHttpClientFactory clientFactory) 30 | { 31 | Dictionary dic = new Dictionary() 32 | { 33 | {"grant_type","client_credentials" }, 34 | {"client_id", clientInfo.ClientID }, 35 | {"client_secret", clientInfo.ClientSecret } 36 | }; 37 | var httpClient = clientFactory.CreateClient(HTTP_CLIENT_NAME); 38 | 39 | httpClient.BaseAddress = new Uri(clientInfo.AuthUrl); 40 | ClientCertificateToken bt = await httpClient.SubmitFormAsync("/connect/token", dic); 41 | if (bt != null && !String.IsNullOrEmpty(bt.access_token)) 42 | { 43 | return bt; 44 | } 45 | 46 | return null; 47 | } 48 | } 49 | 50 | 51 | public class DelegateCertificateProcessor : CertificateProcessor 52 | { 53 | private readonly Func> _proc; 54 | public DelegateCertificateProcessor(Func> requestProc) 55 | { 56 | _proc = requestProc; 57 | } 58 | 59 | public override async Task GetToken(ClientCertificateInfo clientInfo, IHttpClientFactory clientFactory) 60 | { 61 | HttpClient httpClient = clientFactory.CreateClient(HTTP_CLIENT_NAME); 62 | 63 | if (!string.IsNullOrEmpty(clientInfo.AuthUrl)) 64 | { 65 | httpClient.BaseAddress = new Uri(clientInfo.AuthUrl); 66 | } 67 | 68 | var token = await _proc(clientInfo, httpClient); 69 | 70 | return token; 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Extensions/Logger/TestLoggerProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Microsoft.Extensions.Logging; 6 | using Serilog.Context; 7 | 8 | namespace Xfrogcn.AspNetCore.Extensions 9 | { 10 | /// 11 | /// 测试日志提供器 12 | /// 13 | public class TestLoggerProvider : ILoggerProvider, ISupportExternalScope 14 | { 15 | class TestLogger : ILogger 16 | { 17 | internal IExternalScopeProvider ScopeProvider { get; set; } 18 | protected readonly TestLogContent _logContent; 19 | protected readonly string _categoryName; 20 | 21 | public TestLogger(string categoryName, TestLogContent logContent) 22 | { 23 | _categoryName = categoryName; 24 | _logContent = logContent; 25 | } 26 | public IDisposable BeginScope(TState state) 27 | { 28 | return ScopeProvider?.Push(state); 29 | } 30 | 31 | public bool IsEnabled(LogLevel logLevel) 32 | { 33 | return logLevel != LogLevel.None; 34 | } 35 | 36 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) 37 | { 38 | TestLogItem item = new TestLogItem() 39 | { 40 | CategoryName = _categoryName, 41 | EventId = eventId, 42 | LogLevel = logLevel, 43 | Message = formatter(state, exception), 44 | ScopeValues = new List() 45 | }; 46 | 47 | ScopeProvider?.ForEachScope((obj, state) => 48 | { 49 | item.ScopeValues.Add(obj); 50 | }, item); 51 | _logContent.AddLogItem(item); 52 | } 53 | } 54 | 55 | readonly ConcurrentDictionary _loggers; 56 | readonly TestLogContent _logContent; 57 | IExternalScopeProvider _scopeProvider = null; 58 | 59 | public TestLoggerProvider(TestLogContent logContent) 60 | { 61 | _loggers = new ConcurrentDictionary(); 62 | _logContent = logContent; 63 | } 64 | 65 | public ILogger CreateLogger(string categoryName) 66 | { 67 | return _loggers.GetOrAdd(categoryName, key => new TestLogger(key, _logContent) 68 | { 69 | ScopeProvider = _scopeProvider 70 | }); 71 | } 72 | 73 | public void SetScopeProvider(IExternalScopeProvider scopeProvider) 74 | { 75 | _scopeProvider = scopeProvider; 76 | 77 | foreach (var logger in _loggers) 78 | { 79 | logger.Value.ScopeProvider = _scopeProvider; 80 | } 81 | } 82 | 83 | public void Dispose() 84 | { 85 | 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Extensions/Utils/JsonHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text.Encodings.Web; 4 | using System.Text.Json; 5 | using System.Text.Json.Serialization; 6 | using System.Text.Unicode; 7 | using System.Threading.Tasks; 8 | 9 | namespace Xfrogcn.AspNetCore.Extensions 10 | { 11 | public class JsonHelper 12 | { 13 | private JsonSerializerOptions setting = null; 14 | 15 | /// 16 | /// 使用默认序列化设置:日期IsoDateFormat,日期格式 yyyy-MM-dd HH:mm:ss,时区 Local,忽略空字符,缩进模式,驼峰格式 枚举字面转换 17 | /// 18 | public JsonHelper() : this( 19 | new JsonSerializerOptions() 20 | { 21 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 22 | PropertyNameCaseInsensitive = true, //属性不区分大小写 23 | WriteIndented = true, 24 | IgnoreNullValues = true 25 | }) 26 | { 27 | setting.Converters.Add(new JsonStringEnumConverter()); 28 | setting.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; 29 | } 30 | 31 | /// 32 | /// 使用完全自定义的序列化 33 | /// 34 | /// 35 | public JsonHelper(JsonSerializerOptions setting) 36 | { 37 | this.setting = setting; 38 | } 39 | 40 | 41 | 42 | public string ToJson(object obj) 43 | { 44 | if (obj == null) 45 | return ""; 46 | return JsonSerializer.Serialize(obj, setting); 47 | } 48 | 49 | public Task ToJsonAsync(Stream stream, object obj) 50 | { 51 | if (stream == null || obj == null) 52 | { 53 | return Task.CompletedTask; 54 | } 55 | var jsonUtf8Bytes = JsonSerializer.SerializeToUtf8Bytes(obj, setting); 56 | BinaryWriter sw = new BinaryWriter(stream); 57 | sw.Write(jsonUtf8Bytes); 58 | return Task.CompletedTask; 59 | } 60 | 61 | public object ToObject(string json, Type type) 62 | { 63 | if (String.IsNullOrEmpty(json)) 64 | { 65 | return null; 66 | } 67 | 68 | return JsonSerializer.Deserialize(json, type, setting); 69 | } 70 | 71 | public TObject ToObject(string json) 72 | { 73 | return (TObject)ToObject(json, typeof(TObject)); 74 | } 75 | 76 | public ValueTask ToObjectAsync(Stream stream, Type type) 77 | { 78 | if (stream == null) 79 | { 80 | return new ValueTask(null); 81 | } 82 | return JsonSerializer.DeserializeAsync(stream, type, setting); 83 | } 84 | 85 | public ValueTask ToObjectAsync(Stream stream) 86 | { 87 | if (stream == null) 88 | { 89 | return new ValueTask(null); 90 | } 91 | return JsonSerializer.DeserializeAsync(stream, setting); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Extensions/WebApiHostBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using Serilog; 4 | using Serilog.Core; 5 | using Serilog.Events; 6 | using System; 7 | using System.Text; 8 | using Xfrogcn.AspNetCore.Extensions; 9 | 10 | namespace Microsoft.AspNetCore.Hosting 11 | { 12 | public static class WebApiHostBuilderExtensions 13 | { 14 | public static Logger InnerLogger = null; 15 | 16 | 17 | public static IWebHostBuilder UseExtensions(this IWebHostBuilder builder, string[] args, Action configAction = null, Action configureLogger = null) 18 | { 19 | if (builder == null) throw new ArgumentNullException(nameof(builder)); 20 | 21 | AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; 22 | 23 | // 先初始化一个日志,以便在其他配置中可以先行使用 24 | var tempLogger = new LoggerConfiguration() 25 | .MinimumLevel.Is(LogEventLevel.Verbose); 26 | tempLogger = tempLogger.WriteTo.Console(); 27 | var _logger = tempLogger.CreateLogger(); 28 | InnerLogger = _logger; 29 | 30 | 31 | 32 | // 注意 Host的初始化流程 先以此执行 ConfigureAppConfiguration, 最后执行 ConfigureServices 33 | builder = builder.ConfigureServices((context, collection) => 34 | { 35 | collection.AddExtensions(context.Configuration, configAction, configureLogger); 36 | 37 | var config = ServiceCollectionExtensions.config; 38 | StringBuilder sb = new StringBuilder(); 39 | foreach (string h in config.HttpHeaders) 40 | { 41 | if (sb.Length > 0) 42 | { 43 | sb.Append(", "); 44 | } 45 | sb.Append(h); 46 | } 47 | 48 | InnerLogger.Information($"初始化完成:\n" + 49 | $"\t系统日志级别:{config.SystemLogLevel}\n" + 50 | $"\t应用日志级别:{config.AppLogLevel}\n" + 51 | $"\tEF Core Command日志级别:{config.EFCoreCommandLevel}\n" + 52 | $"\t是否开启控制台日志:{config.ConsoleLog}\n" + 53 | $"\t服务端请求日志记录级别:{config.ServerRequestLevel}\n" + 54 | $"\t客户端请求日志级别:{config.ClientRequestLevel}\n" + 55 | $"\t是否开启客户端请求日志:{config.EnableClientRequestLog}\n" + 56 | $"\t日志保留天数:{config.MaxLogDays}\n" + 57 | $"\t记录以下HTTP请求头:{sb.ToString()}\n" + 58 | $"\tURLS:{context.Configuration["URLS"]}\n"); 59 | 60 | }); 61 | 62 | 63 | 64 | return builder; 65 | } 66 | 67 | 68 | 69 | private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) 70 | { 71 | if (e.ExceptionObject != null && e.ExceptionObject is Exception) 72 | { 73 | Log.Logger.Fatal(e.ExceptionObject as Exception, "系统异常"); 74 | } 75 | else 76 | { 77 | Log.Logger.Fatal($"系统异常"); 78 | } 79 | } 80 | 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Extensions.RedisQueueProducer/RedisConnectionManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Logging; 6 | using StackExchange.Redis; 7 | 8 | namespace Xfrogcn.AspNetCore.Extensions.RedisQueueProducer 9 | { 10 | public class RedisConnectionManager 11 | { 12 | readonly ILogger _logger; 13 | private static volatile ConcurrentDictionary _connections = new ConcurrentDictionary(); 14 | private static readonly ConcurrentDictionary _connectionLocks = new ConcurrentDictionary(); 15 | 16 | public RedisConnectionManager(ILogger logger) 17 | { 18 | _logger = logger; 19 | } 20 | 21 | public async Task ConnectAsync(RedisOptions redisOptions, CancellationToken token) 22 | { 23 | int _hashCode = GetRedisOptionsHashCode(redisOptions); 24 | 25 | token.ThrowIfCancellationRequested(); 26 | 27 | ConnectionMultiplexer connection = null; 28 | _connections.TryGetValue(_hashCode, out connection); 29 | if (connection != null) 30 | { 31 | return connection; 32 | } 33 | 34 | var connectionLock = _connectionLocks.GetOrAdd(_hashCode, k => 35 | { 36 | return new SemaphoreSlim(initialCount: 1, maxCount: 1); 37 | }); 38 | 39 | await connectionLock.WaitAsync(token); 40 | try 41 | { 42 | _connections.TryGetValue(_hashCode, out connection); 43 | if (connection != null) 44 | { 45 | return connection; 46 | } 47 | 48 | if (redisOptions.ConfigurationOptions != null) 49 | { 50 | connection = await ConnectionMultiplexer.ConnectAsync(redisOptions.ConfigurationOptions); 51 | } 52 | else 53 | { 54 | connection = await ConnectionMultiplexer.ConnectAsync(redisOptions.Configuration); 55 | } 56 | _logger.LogInformation("创建新的Redis链接"); 57 | _connections.TryAdd(_hashCode, connection); 58 | 59 | } 60 | catch (Exception e) 61 | { 62 | _logger.LogError(e, $"Connect Error"); 63 | } 64 | finally 65 | { 66 | connectionLock.Release(); 67 | } 68 | return connection; 69 | } 70 | 71 | private int GetRedisOptionsHashCode(RedisOptions redisOptions) 72 | { 73 | string cfg = ""; 74 | if (redisOptions.ConfigurationOptions != null) 75 | { 76 | cfg = System.Text.Json.JsonSerializer.Serialize(redisOptions.ConfigurationOptions); 77 | } 78 | else 79 | { 80 | cfg = redisOptions.Configuration; 81 | } 82 | return cfg.GetHashCode(); 83 | } 84 | 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /doc/TokenProvider.md: -------------------------------------------------------------------------------- 1 | # 认证令牌处理 2 | 3 | 在真实项目中,服务之间的交互式常态,特别是在微服务体系下,而服务之间的交互常常需要提供认证信息(如基础认证、Token认证等),认证过程通常包括获取令牌、令牌缓存、令牌的失效判断、令牌的重新获取等,如果这些处理都由每个业务系统自己处理,是非常繁琐的。 4 | 5 | ## 认证提供器 6 | 7 | 认证令牌通过认证提供器进行统一管理, 认证提供器包含以下组件,他们之间相互协同来实现令牌的自动管理: 8 | 9 | - 客户端信息,用于记录认证客户端信息(如clientId,client密钥等) 10 | - 认证管理器,用于管理特定客户端的认证过程 11 | - 认证处理器,用于获取令牌 12 | - 令牌设置器,用于将令牌设置到Http请求中 13 | - 令牌缓存管理器,用于管理令牌缓存 14 | - 应答检查器,用于检查Http应答是否为未授权 15 | - 认证提供器,用于获取指定客户端的认证管理器 16 | - 认证设置,用于配置指定客户端的认证信息 17 | 18 | ## 配置 19 | 20 | 1. 启用认证提供器 21 | 22 | 如果通过了UseExtensions或者AddExtensions方式启用了扩展库功能,默认已经开启认证提供器,如果想单独使用认证提供器,可使用IServiceCollection的AddClientTokenProvider扩展方法: 23 | 24 | ```c# 25 | IServiceCollection sc = new ServiceCollection() 26 | .AddClientTokenProvider(); 27 | ``` 28 | 29 | 2. 配置客户端 30 | 31 | 有两种方式配置认证客户端信息,一是与AddClientTokenProvider集合,传入认证配置委托: 32 | 33 | ```c# 34 | sc.AddClientTokenProvider(options => 35 | { 36 | options.AddClient("", "TestClient", "") 37 | // 设置此客户端使用Basic认证 38 | .UseBasicAuth("test", "test"); 39 | }); 40 | ``` 41 | 42 | 二是,直接通过AddTokenClient方法: 43 | 44 | ```c# 45 | serviceDescriptors.AddTokenClient(url, clientId, "", options=> 46 | { 47 | options.UseBasicAuth(userName, password); 48 | }); 49 | ``` 50 | 51 | ## 内置的认证方式 52 | 53 | 令牌提供器内置了两种认证方式: 54 | 55 | - 基本认证:UseBasicAuth 56 | - OIDC认证:UseOIDCAuth 57 | 58 | ## 内置的认证处理器 59 | 60 | 扩展库提供了OIDC认证处理器,通过SetProcessor方法可进行配置: 61 | 62 | ```c# 63 | sc.AddClientTokenProvider(options => 64 | { 65 | options.AddClient("", "TestClient", "") 66 | .SetProcessor(CertificateProcessor.OIDC); 67 | }); 68 | ``` 69 | 70 | ## 内置的令牌缓存管理器 71 | 72 | 内部提供两种缓存管理器: 73 | 74 | - 本地缓存: 令牌信息缓存到本机 75 | - 分布式缓存:通过注入的IDistributedCache来缓存令牌 76 | 77 | 可以通过SetTokenCacheManager方法来指定缓存管理器工厂: 78 | 79 | ```c# 80 | sc.AddClientTokenProvider(options => 81 | { 82 | options.AddClient("", "TestClient", "") 83 | // 本地缓存 84 | // .SetTokenCacheManager( TokenCacheManager.MemoryCacheFactory ) 85 | // 分布式缓存 86 | .SetTokenCacheManager(TokenCacheManager.DistributedCacheFactory); 87 | }); 88 | ``` 89 | 90 | ## 内置的令牌设置器 91 | 92 | 扩展库提供两种令牌设置器: 93 | 94 | - Bearer:将令牌设置到认证请求头 95 | - QueryString:将令牌附加到请求的查询字符串中 96 | 97 | 可通过SetTokenSetter进行配置: 98 | 99 | ```c# 100 | sc.AddClientTokenProvider(options => 101 | { 102 | options.AddClient("", "TestClient", "") 103 | // .SetTokenSetter(SetTokenProcessor.Bearer) 104 | .SetTokenSetter(SetTokenProcessor.QueryString); 105 | }); 106 | ``` 107 | 108 | ## 自定义 109 | 110 | 如果内置的认证方式或组件无法满足需求,可替换实现相应的自定义组件 111 | 112 | 1. 自定义认证处理器 113 | 114 | 自定义认证处理器支持两种方式: 115 | 116 | - 实现一个CertificateProcessor的子类 117 | - 支持配置一个Func<ClientCertificateInfo, HttpClient, Task<ClientCertificateToken>>处理委托, 该委托传入认证客户端信息、请求Token所使用的HttpClient,需要委托返回令牌信息 118 | 119 | 2. 自定义令牌缓存管理器 120 | 121 | 要自定义令牌缓存管理器,需要实现自己的TokenCacheManager,然后通过SetTokenCacheManager来设置缓存管理器的创建委托。 122 | 123 | 3. 自定义令牌设置器 124 | 125 | 自定义令牌设置器支持两种方式: 126 | 127 | - 实现一个SetTokenProcessor的子类 128 | - 支持配置一个Func<HttpRequestMessage, string, Task>处理委托, 该委托传入当前请求消息以及令牌字符串 129 | 130 | 4. 自定义应答检查器 131 | 132 | 应答检查器用于从Http应答中判断令牌是否失效,要实现自己的应答检查器,只需实现CheckResponseProcessor的子类,然后通过SetResponseChecker来配置 133 | -------------------------------------------------------------------------------- /doc/LightweightMapper.md: -------------------------------------------------------------------------------- 1 | # 轻量级的实体映射 2 | 3 | 轻量级的实体映射用于数据层、DTO对象、视图层的实体自动转换或自动拷贝。 4 | 5 | 核心功能 6 | 7 | - 在使用之前无需手动定义类型之间的映射关系 8 | - 采用动态编译、缓存转换委托,提升性能。 9 | - 支持通过特性定义属性映射关系 10 | - 支持插入自定义的转换处理方法 11 | - 支持列表转换 12 | - 支持嵌套类型转换 13 | - 支持循环引用及引用关系维持 14 | - 支持转换模式或拷贝模式 15 | - 支持生成预定义的拷贝委托 16 | - 为了保持其轻量性,目前支持以下转换 17 | - 值类型转换 18 | - 数值类型之间的兼容转换(如int-->uint) 19 | - 支持值类型与其可空类型间的兼容转换 20 | - 字典类型转换 21 | - 列表类型转换 22 | - 枚举类型与string类型间的转换 23 | - **不支持**结构体之间的转换以及结构体与类之间的转换 24 | 25 | ## 启用 26 | 27 | 启用轻量级的实体映射,有两种方式: 28 | 29 | - 如果你是和扩展库其他功能同时使用,可直接通过UseExtensions即可 30 | 31 | ```c# 32 | using IHost host = Host.CreateDefaultBuilder() 33 | // UseExtensions会自动注入Mapper 34 | .UseExtensions() 35 | .ConfigureServices(sc => 36 | { 37 | // 通过ConfigureLightweightMapper来配置映射 38 | sc.ConfigureLightweightMapper(options => 39 | { 40 | // 41 | }); 42 | }) 43 | .Build(); 44 | ``` 45 | 46 | - 如果你需要单独使用,可通过IServiceCollection上的AddLightweightMapper方法启用 47 | 48 | ```c# 49 | //实体转换 50 | serviceDescriptors.AddLightweightMapper() 51 | .ConfigureLightweightMapper(options => 52 | { 53 | // 54 | }); 55 | ``` 56 | 57 | ## 配置自定义转换逻辑 58 | 59 | 你可以通过映射设置上的AddConvert来配置对应设置实体转换的`后置`逻辑,如下所示。 60 | 61 | ```c# 62 | //实体转换 63 | serviceDescriptors.AddLightweightMapper() 64 | .ConfigureLightweightMapper(options => 65 | { 66 | // 通过AddConvert可自定义转换逻辑 67 | // 以下定义从SourceA转换到TargetB时,自动设置属性C的值 68 | options.AddConvert((mapper, a, b) => 69 | { 70 | b.C = "C"; 71 | }); 72 | }); 73 | ``` 74 | 75 | ## 使用 76 | 77 | 你可以通过IMapperProvider的GetMapper方法或IMapper<,>直接获取Mapper实例。 78 | 79 | - 通过IMapperProvider 80 | 81 | ```c# 82 | // 通过IMapperProvider 83 | var mapperProvider = host.Services.GetRequiredService(); 84 | var mapper = mapperProvider.GetMapper(); 85 | var targetA = mapper.Convert(sourceA); 86 | ``` 87 | 88 | - 通过IMapper<,> 89 | 90 | ```c# 91 | var mapperB = host.Services.GetRequiredService>(); 92 | var targetB = mapperB.Convert(sourceA); 93 | ``` 94 | 95 | ## 通过特性指定属性映射关系 96 | 97 | 默认映射按照属性名称进行,你也可以通过MapperPropertyNameAttribute特性进行指定。 98 | 99 | MapperPropertyNameAttribute: 100 | 101 | | 属性名 | 类型 | 说明 | 102 | | ----- | ---- | ---- | 103 | | Name | String | 目标或源的名称 | 104 | | TargetType | Type | 映射到的目标类型 | 105 | | SourceType | Type | 映射到当前类型的来源类型 | 106 | 107 | 通过SourceType或TargetType你可以根据需求灵活的在源类型或目标类型上设置映射关系。 108 | 109 | ## 拷贝 110 | 111 | 实体映射也提供了拷贝方法,通过该方法可以将源实体属性拷贝到目标实体。 112 | 113 | - 通过IMapper<,>的CopyTo方法进行默认拷贝: 114 | 115 | ```c# 116 | var mapperB = host.Services.GetRequiredService>(); 117 | var targetB1 = new TargetB(); 118 | mapperB.CopyTo(sourceA, targetB1); 119 | ``` 120 | 121 | - 通过DefineCopyTo方法定义排除字段外的拷贝委托 122 | 123 | ```c# 124 | var mapperB = host.Services.GetRequiredService>(); 125 | // 只拷贝指定字段之外的属性 126 | var copyProc = mapperB.DefineCopyTo(a => 127 | new 128 | { 129 | a.A //忽略属性A 130 | }); 131 | var targetB2 = new TargetB(); 132 | copyProc(sourceA, targetB2); 133 | ``` 134 | 135 | ## 示例 136 | 137 | 以上代码的完整实例可参考`./examples/AutoMapper`目录 138 | -------------------------------------------------------------------------------- /src/Extensions/HttpClient/HttpClientBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Http; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | using Xfrogcn.AspNetCore.Extensions; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection 7 | { 8 | public static class HttpClientBuilderExtensions 9 | { 10 | public static string DisableEnsureSuccessStatusCode_Key = "DisableEnsureSuccessStatusCode"; 11 | 12 | /// 13 | /// 添加一个Mock Http消息处理层,用于单元测试 14 | /// 15 | /// 16 | /// 17 | public static MockHttpMessageHandlerOptions AddMockHttpMessageHandler(this IHttpClientBuilder builder) 18 | { 19 | MockHttpMessageHandlerOptions options = new MockHttpMessageHandlerOptions(); 20 | builder.ConfigurePrimaryHttpMessageHandler(() => 21 | { 22 | return new MockHttpMessageHandler(options); 23 | }); 24 | return options; 25 | } 26 | 27 | /// 28 | /// 添加一个消息处理器,通过此消息处理器会在请求过程中,自动从认证服务获取令牌并添加到请求头中 29 | /// 该处理器通过获取对应客户端的认证令牌 30 | /// 31 | /// HttpClient构建器 32 | /// 认证客户端Id 33 | /// 34 | public static IHttpClientBuilder AddTokenMessageHandler(this IHttpClientBuilder builder, string clientId) 35 | { 36 | builder.AddHttpMessageHandler((sp) => 37 | { 38 | IClientCertificateProvider provider = sp.GetRequiredService(); 39 | ClientCertificateManager cm = provider.GetClientCertificateManager(clientId); 40 | return new GetClientTokenMessageHandler(cm); 41 | }); 42 | return builder; 43 | } 44 | 45 | /// 46 | /// 禁止HttpClient Extensions 方法自动调用EnsureSuccessStatusCode 47 | /// 48 | /// 49 | /// 50 | public static IHttpClientBuilder DisableEnsureSuccessStatusCode(this IHttpClientBuilder builder) 51 | { 52 | builder.AddHttpMessageHandler((sp) => 53 | { 54 | return new DisableEnsureSuccessStatusCodeMessageHandler(); 55 | }); 56 | return builder; 57 | } 58 | 59 | 60 | 61 | public static void DisableEnsureSuccessStatusCode(this HttpRequestMessage request) 62 | { 63 | if (request.Properties.ContainsKey(DisableEnsureSuccessStatusCode_Key)) 64 | { 65 | request.Properties[DisableEnsureSuccessStatusCode_Key] = true; 66 | } 67 | else 68 | { 69 | request.Properties.TryAdd(DisableEnsureSuccessStatusCode_Key, true); 70 | } 71 | } 72 | 73 | public static bool IsDisableEnsureSuccessStatusCode(this HttpRequestMessage request) 74 | { 75 | if (request.Properties.ContainsKey(DisableEnsureSuccessStatusCode_Key)) 76 | { 77 | return (bool)request.Properties[DisableEnsureSuccessStatusCode_Key]; 78 | } 79 | return false; 80 | } 81 | 82 | public static void IgnoreRequestToken(this HttpRequestMessage request) 83 | { 84 | if (!request.Properties.ContainsKey(GetClientTokenMessageHandler.IGNORE_TOKEN_PROPERTY)) 85 | { 86 | request.Properties.Add(GetClientTokenMessageHandler.IGNORE_TOKEN_PROPERTY, true); 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/Extensions.Tests/TokenProvider/CertificateProcessorTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Specialized; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Xfrogcn.AspNetCore.Extensions; 7 | using Xunit; 8 | 9 | namespace Extensions.Tests.TokenProvider 10 | { 11 | [Trait("", "TokenProvider")] 12 | public class CertificateProcessorTest 13 | { 14 | [Fact(DisplayName = "OIDC Token获取")] 15 | public async Task OIDCProcessor_Test() 16 | { 17 | IServiceCollection sc = new ServiceCollection(); 18 | HttpRequestMessage request = null; 19 | sc.AddHttpClient(CertificateProcessor.OIDCCertificateProcessor.HTTP_CLIENT_NAME) 20 | .AddMockHttpMessageHandler() 21 | .AddMock("*", HttpMethod.Post, async (req, res) => 22 | { 23 | request = req; 24 | await res.WriteObjectAsync(new ClientCertificateToken() 25 | { 26 | access_token = "1", 27 | token_type = "1", 28 | expires_in = 60 29 | }); 30 | }); 31 | 32 | var sp = sc.BuildServiceProvider(); 33 | 34 | IHttpClientFactory factory = sp.GetRequiredService(); 35 | 36 | var processor = CertificateProcessor.OIDC; 37 | var token = await processor.GetToken(new ClientCertificateInfo() 38 | { 39 | ClientID = "1", 40 | ClientName = "1", 41 | ClientSecret = "1", 42 | AuthUrl = "https://auth.com" 43 | }, factory); 44 | 45 | var reqStr = await request.GetObjectAsync(); 46 | 47 | Assert.Equal("grant_type=client_credentials&client_id=1&client_secret=1", reqStr); 48 | 49 | } 50 | 51 | 52 | [Fact(DisplayName = "委托Token获取")] 53 | public async Task DelegateProcessor_Test() 54 | { 55 | IServiceCollection sc = new ServiceCollection(); 56 | HttpRequestMessage request = null; 57 | sc.AddHttpClient(CertificateProcessor.HTTP_CLIENT_NAME) 58 | .AddMockHttpMessageHandler() 59 | .AddMock("*", HttpMethod.Get, async (req, res) => 60 | { 61 | request = req; 62 | await res.WriteObjectAsync(new ClientCertificateToken() 63 | { 64 | access_token = "1", 65 | token_type = "1", 66 | expires_in = 60 67 | }); 68 | }); 69 | 70 | var sp = sc.BuildServiceProvider(); 71 | 72 | IHttpClientFactory factory = sp.GetRequiredService(); 73 | 74 | var processor = CertificateProcessor.CreateDelegateProcessor(async (ci, client)=> 75 | { 76 | return await client.GetAsync("/cgi-bin/token", new NameValueCollection() 77 | { 78 | { "grant_type", "client_credential" }, 79 | { "appid", ci.ClientID }, 80 | { "secret", ci.ClientSecret } 81 | }); 82 | }); 83 | var token = await processor.GetToken(new ClientCertificateInfo() 84 | { 85 | ClientID = "1", 86 | ClientName = "1", 87 | ClientSecret = "1", 88 | AuthUrl = "https://auth.com" 89 | }, factory); 90 | 91 | string reqStr = request.RequestUri.AbsoluteUri; 92 | 93 | Assert.Equal("https://auth.com/cgi-bin/token?grant_type=client_credential&appid=1&secret=1", reqStr); 94 | 95 | } 96 | 97 | 98 | 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Extensions/TokenProvider/SetTokenProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Collections.Generic; 8 | 9 | namespace Xfrogcn.AspNetCore.Extensions 10 | { 11 | /// 12 | /// 设置令牌的处理器 13 | /// 14 | public abstract class SetTokenProcessor 15 | { 16 | public abstract Task SetTokenAsync(HttpRequestMessage request, string token); 17 | 18 | 19 | public static readonly SetTokenProcessor Bearer = new SetBearerTokenProcessor(); 20 | 21 | public static readonly SetTokenProcessor QueryString = new QueryStringTokenProcessor(); 22 | 23 | public static DelegateSetTokenProcessor CreateDelegateSetTokenProcessor(Func setter) 24 | { 25 | return new DelegateSetTokenProcessor(setter); 26 | } 27 | 28 | 29 | /// 30 | /// Bearer令牌 31 | /// 32 | public class SetBearerTokenProcessor : SetTokenProcessor 33 | { 34 | public override Task SetTokenAsync(HttpRequestMessage request, string token) 35 | { 36 | request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); 37 | return Task.CompletedTask; 38 | 39 | } 40 | } 41 | 42 | /// 43 | /// 通过查询字符串传递访问令牌 44 | /// 45 | public class QueryStringTokenProcessor : SetTokenProcessor 46 | { 47 | private readonly string _queryKey = ""; 48 | public QueryStringTokenProcessor() : this("access_token") 49 | { 50 | 51 | } 52 | 53 | public QueryStringTokenProcessor(string queryKey) 54 | { 55 | _queryKey = queryKey; 56 | if (string.IsNullOrEmpty(_queryKey)) 57 | { 58 | _queryKey = "access_token"; 59 | } 60 | } 61 | 62 | public override Task SetTokenAsync(HttpRequestMessage request, string token) 63 | { 64 | UriBuilder ub = new UriBuilder(request.RequestUri); 65 | var qs = System.Web.HttpUtility.ParseQueryString(request.RequestUri.Query); 66 | qs[_queryKey] = token; 67 | StringBuilder sb = new StringBuilder(); 68 | var kl = qs.AllKeys; 69 | foreach (string k in kl) 70 | { 71 | if (sb.Length > 0) 72 | { 73 | sb.Append("&"); 74 | } 75 | sb.Append(k).Append("="); 76 | if (!String.IsNullOrEmpty(qs[k])) 77 | { 78 | 79 | sb.Append(System.Net.WebUtility.UrlEncode(qs[k])); 80 | } 81 | } 82 | ub.Query = sb.ToString(); 83 | request.RequestUri = ub.Uri; 84 | 85 | return Task.CompletedTask; 86 | } 87 | } 88 | 89 | public class DelegateSetTokenProcessor : SetTokenProcessor 90 | { 91 | private readonly Func _setter; 92 | public DelegateSetTokenProcessor(Func setter) 93 | { 94 | _setter = setter; 95 | } 96 | 97 | public override Task SetTokenAsync(HttpRequestMessage request, string token) 98 | { 99 | if (_setter!=null) 100 | { 101 | return _setter(request, token); 102 | } 103 | return Task.CompletedTask; 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/DefaultParallelQueueConsumerFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Options; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Collections.Concurrent; 6 | using Microsoft.Extensions.Logging; 7 | using System.Collections.Generic; 8 | 9 | namespace Xfrogcn.AspNetCore.Extensions 10 | { 11 | /// 12 | /// 默认并行队列处理器工厂 13 | /// 14 | public class DefaultParallelQueueConsumerFactory : IParallelQueueConsumerFactory 15 | { 16 | private readonly IServiceProvider _serviceProvider; 17 | private readonly ILoggerFactory _loggerFactory; 18 | 19 | class ConsumerCacheItem : IEqualityComparer 20 | { 21 | public Type EntityType { get; set; } 22 | 23 | public Type StateType { get; set; } 24 | 25 | public string Name { get; set; } 26 | 27 | 28 | public bool Equals([AllowNull] ConsumerCacheItem x, [AllowNull] ConsumerCacheItem y) 29 | { 30 | if (x == y) 31 | { 32 | return true; 33 | } 34 | if (x == null || y == null) 35 | { 36 | return false; 37 | } 38 | 39 | if (x.EntityType == y.EntityType && 40 | x.StateType == y.StateType && 41 | (x.Name ?? "").Equals(y.Name, StringComparison.OrdinalIgnoreCase)) 42 | { 43 | return true; 44 | } 45 | 46 | return false; 47 | } 48 | 49 | public int GetHashCode([DisallowNull] ConsumerCacheItem obj) 50 | { 51 | string str = $"{obj.EntityType.FullName}-{obj.StateType.FullName}-{obj.Name ?? ""}".ToLower(); 52 | 53 | return str.GetHashCode(); 54 | } 55 | } 56 | 57 | private ConcurrentDictionary _cache = new ConcurrentDictionary(new ConsumerCacheItem()); 58 | 59 | 60 | public DefaultParallelQueueConsumerFactory(IServiceProvider serviceProvider, ILoggerFactory loggerFactory) 61 | { 62 | _serviceProvider = serviceProvider; 63 | _loggerFactory = loggerFactory; 64 | } 65 | 66 | public IParallelQueueConsumer CreateConsumer(string name, TState state) 67 | { 68 | ConsumerCacheItem cacheKey = new ConsumerCacheItem() 69 | { 70 | EntityType = typeof(TEntity), 71 | StateType = typeof(TState), 72 | Name = name??"" 73 | }; 74 | 75 | var instance = _cache.GetOrAdd(cacheKey, (key, sp) => 76 | { 77 | using (var scope = sp.CreateScope()) 78 | { 79 | var optionsManager = scope.ServiceProvider.GetRequiredService>>(); 80 | var options = optionsManager.Get(name); 81 | if (options.ExecuteDelegate == null) 82 | { 83 | throw new InvalidOperationException("必须设置执行委托ExecuteDelegate"); 84 | } 85 | DefaultParallelQueueConsumer consumer = new DefaultParallelQueueConsumer( 86 | options, 87 | _serviceProvider, 88 | name ?? "", 89 | state, 90 | _loggerFactory 91 | ); 92 | return consumer; 93 | } 94 | }, _serviceProvider); 95 | 96 | 97 | return (IParallelQueueConsumer)instance; 98 | 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Extensions/TokenProvider/ClientCertificateProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace Xfrogcn.AspNetCore.Extensions 9 | { 10 | public class ClientCertificateProvider : IClientCertificateProvider 11 | { 12 | readonly ClientCertificateOptions _options; 13 | readonly ILoggerFactory _loggerFactory; 14 | readonly IHttpClientFactory _httpFactory; 15 | readonly ILogger _logger; 16 | readonly IServiceProvider _serviceProvider; 17 | readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); 18 | 19 | static readonly Action _logGetClient = 20 | LoggerMessage.Define(LogLevel.Debug, new EventId(20, "GetClient"), "获取客户端:clientId: {clientId}, clientName: {clientName}, url: {url}"); 21 | 22 | public ClientCertificateProvider( 23 | IOptions options, 24 | ILoggerFactory loggerFactory, 25 | IHttpClientFactory httpFactory, 26 | IServiceProvider serviceProvider, 27 | ILogger logger) 28 | { 29 | _options = options.Value; 30 | _loggerFactory = loggerFactory; 31 | _httpFactory = httpFactory; 32 | _serviceProvider = serviceProvider; 33 | 34 | _logger = logger; 35 | 36 | if (_options != null && _options.ClientList != null) 37 | { 38 | _logger.LogDebug($"认证中心Url:{_options.DefaultUrl ?? ""}"); 39 | foreach (var ci in _options.ClientList) 40 | { 41 | _logger.LogDebug($"客户端:id: {ci.ClientID} name: {ci.ClientName ?? ""} url: {ci.AuthUrl ?? ""}"); 42 | } 43 | } 44 | 45 | } 46 | 47 | public ClientCertificateManager GetClientCertificateManager(string clientId) 48 | { 49 | _logger.LogDebug($"获取客户端管理器:{clientId??""}"); 50 | var cm = _cache.GetOrAdd(clientId, (id) => 51 | { 52 | var client =_options.ClientList.FirstOrDefault(c => c.ClientID == id); 53 | if( client == null) 54 | { 55 | return null; 56 | } 57 | if (String.IsNullOrEmpty(client.AuthUrl)) 58 | { 59 | client.AuthUrl = _options.DefaultUrl; 60 | } 61 | 62 | TokenCacheManager cacheManager = null; 63 | if(client.TokenCacheManager != null) 64 | { 65 | cacheManager = client.TokenCacheManager(_serviceProvider, clientId); 66 | } 67 | else 68 | { 69 | cacheManager = TokenCacheManager.MemoryCacheFactory(_serviceProvider, clientId); 70 | } 71 | 72 | return new ClientCertificateManager( 73 | client, 74 | client.Processor ?? CertificateProcessor.OIDC, 75 | client.TokenSetter ?? SetTokenProcessor.Bearer, 76 | client.ResponseChecker ?? CheckResponseProcessor.NormalChecker, 77 | cacheManager, 78 | _loggerFactory.CreateLogger(), 79 | _httpFactory); 80 | }); 81 | 82 | if(cm == null) 83 | { 84 | _logger.LogError($"客户端未配置:{clientId}"); 85 | throw new InvalidOperationException($"客户端未配置:{clientId}"); 86 | } 87 | else 88 | { 89 | _logGetClient(_logger, clientId, cm.Client?.ClientName, cm.Client?.AuthUrl, null); 90 | } 91 | return cm; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Extensions/ParallelQueue/ParallelQueueHostedService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Hosting; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Xfrogcn.AspNetCore.Extensions 10 | { 11 | public class ParallelQueueHostedService : IHostedService 12 | { 13 | private readonly IParallelQueueProducerFactory _producerFactory; 14 | private readonly IParallelQueueConsumerFactory _consumerFactory; 15 | private readonly ILogger> _logger; 16 | private readonly string _queueName; 17 | private readonly TState _state; 18 | private readonly int _timeout; 19 | 20 | private IParallelQueueConsumer _consumer; 21 | private IParallelQueueProducer _producer; 22 | private CancellationTokenSource _stopToken; 23 | 24 | public ParallelQueueHostedService( 25 | IParallelQueueConsumerFactory consumerFactory, 26 | IParallelQueueProducerFactory producerFactory, 27 | string queuqName, 28 | int timeout, 29 | TState state, 30 | ILogger> logger) 31 | { 32 | if (consumerFactory == null) 33 | throw new ArgumentNullException(nameof(consumerFactory)); 34 | if (producerFactory == null) 35 | throw new ArgumentNullException(nameof(producerFactory)); 36 | if (logger == null) 37 | throw new ArgumentNullException(nameof(logger)); 38 | 39 | _consumerFactory = consumerFactory; 40 | _producerFactory = producerFactory; 41 | _queueName = queuqName; 42 | _timeout = timeout; 43 | _state = state; 44 | _logger = logger; 45 | } 46 | 47 | public async Task StartAsync(CancellationToken cancellationToken) 48 | { 49 | if (_producer == null) 50 | { 51 | _producer = _producerFactory.CreateProducer(_queueName); 52 | } 53 | if (_consumer == null) 54 | { 55 | _consumer = _consumerFactory.CreateConsumer(_queueName, _state); 56 | } 57 | 58 | await _consumer.StartAsync(); 59 | _stopToken = new CancellationTokenSource(); 60 | _ = Task.Run(async () => 61 | { 62 | await QueueReciver(); 63 | }); 64 | } 65 | 66 | 67 | private CancellationTokenSource _stoppingToken = null; 68 | public async Task QueueReciver() 69 | { 70 | _logger.LogInformation($"队列接收器已启动:{_queueName}"); 71 | while (!_stopToken.IsCancellationRequested) 72 | { 73 | var (msg, isOk) = await _producer.TryTakeAsync(TimeSpan.FromSeconds(_timeout), _stopToken.Token); 74 | if (msg != null && isOk) 75 | { 76 | while (true) 77 | { 78 | var isAdded = _consumer.TryAdd(msg, TimeSpan.FromSeconds(_timeout)); 79 | if (isAdded) 80 | { 81 | break; 82 | } 83 | } 84 | } 85 | } 86 | _logger.LogInformation($"队列接收器已退出:{_queueName}"); 87 | _stoppingToken.Cancel(); 88 | } 89 | 90 | public async Task StopAsync(CancellationToken cancellationToken) 91 | { 92 | _stoppingToken = new CancellationTokenSource(); 93 | _stopToken.Cancel(); 94 | await _producer.StopAsync(cancellationToken); 95 | await _consumer.StopAsync(); 96 | _stoppingToken.Token.WaitHandle.WaitOne(); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Extensions/TokenProvider/TokenProviderServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | using Xfrogcn.AspNetCore.Extensions; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection 7 | { 8 | public static class TokenProviderServiceCollectionExtensions 9 | { 10 | public static IServiceCollection AddClientTokenProvider(this IServiceCollection serviceDescriptors) 11 | { 12 | serviceDescriptors.Configure((options) =>{}); 13 | serviceDescriptors.TryAddSingleton(); 14 | 15 | return serviceDescriptors; 16 | } 17 | 18 | public static IServiceCollection AddClientTokenProvider(this IServiceCollection serviceDescriptors, IConfiguration configuration) 19 | { 20 | serviceDescriptors.AddClientTokenProvider(); 21 | 22 | CurrentClientInfo cci = new CurrentClientInfo(); 23 | configuration.Bind(cci); 24 | if (!String.IsNullOrEmpty(cci.ClientID) && !String.IsNullOrEmpty(cci.ClientSecret)) 25 | { 26 | serviceDescriptors.AddSingleton(cci); 27 | } 28 | 29 | serviceDescriptors.Configure((options) => 30 | { 31 | // 顶层默认设置(当前应用信息) 32 | if (!String.IsNullOrEmpty(cci.ClientID) && !String.IsNullOrEmpty(cci.ClientSecret)) 33 | { 34 | options.AddClient(cci.AuthUrl, cci.ClientID, cci.ClientSecret, cci.ClientName); 35 | } 36 | if (!String.IsNullOrEmpty(cci.AuthUrl)) 37 | { 38 | options.DefaultUrl = cci.AuthUrl; 39 | } 40 | IConfiguration section = configuration?.GetSection("_Clients"); 41 | if (section != null) 42 | { 43 | options.FromConfiguration(section); 44 | } 45 | 46 | }); 47 | 48 | return serviceDescriptors; 49 | } 50 | 51 | public static IServiceCollection AddClientTokenProvider(this IServiceCollection serviceDescriptors, Action optionsAction) 52 | { 53 | serviceDescriptors.AddClientTokenProvider(); 54 | 55 | serviceDescriptors.Configure(optionsAction); 56 | 57 | return serviceDescriptors; 58 | } 59 | 60 | public static IServiceCollection AddTokenClient(this IServiceCollection serviceDescriptors, ClientCertificateInfo ci) 61 | { 62 | if(ci!=null && !string.IsNullOrEmpty(ci.ClientID) && !string.IsNullOrEmpty(ci.ClientSecret)) 63 | { 64 | return AddClientTokenProvider(serviceDescriptors, (options) => 65 | { 66 | options.AddClient(ci.AuthUrl, ci.ClientID, ci.ClientSecret, ci.ClientName); 67 | }); 68 | } 69 | return serviceDescriptors; 70 | } 71 | 72 | public static IServiceCollection AddTokenClient(this IServiceCollection serviceDescriptors, string url, string clientId, string clientSecret, Action clientOptions=null) 73 | { 74 | return AddClientTokenProvider(serviceDescriptors, options => 75 | { 76 | var client = options.AddClient(url, clientId, clientSecret); 77 | if (clientOptions != null) 78 | { 79 | clientOptions(client); 80 | } 81 | }); 82 | } 83 | 84 | /// 85 | /// 配置获取Token的HttpClient 86 | /// 87 | /// 88 | /// 89 | public static IHttpClientBuilder ConfigureTokenHttpClient(this IServiceCollection serviceDescriptors) 90 | { 91 | return serviceDescriptors.AddHttpClient(ClientCertificateManager.HTTP_CLIENT_NAME); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/Extensions.Tests/AutoMapper/GenearatorTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Threading.Tasks; 7 | using Xfrogcn.AspNetCore.Extensions; 8 | using Xfrogcn.AspNetCore.Extensions.AutoMapper; 9 | using Xunit; 10 | 11 | namespace Extensions.Tests.AutoMapper 12 | { 13 | 14 | [Trait("", "Generator")] 15 | public class GenearatorTest 16 | { 17 | enum TestEnum 18 | { 19 | [MapperEnumName("hello")] 20 | A, 21 | B 22 | } 23 | 24 | [Fact(DisplayName = "ValueType")] 25 | public void Test1() 26 | { 27 | 28 | IServiceCollection sc = new ServiceCollection() 29 | .AddLightweightMapper(); 30 | 31 | IServiceProvider sp = sc.BuildServiceProvider(); 32 | IMapperProvider provider = sp.GetRequiredService(); 33 | 34 | // int ==> int 35 | var intConv = GenerateConverter(provider); 36 | Assert.Equal(10, intConv(10)); 37 | 38 | var conv2 = GenerateConverter(provider); 39 | Assert.Equal(0, conv2(null)); 40 | Assert.Equal(10, conv2(10)); 41 | 42 | var conv3 = GenerateConverter(provider); 43 | Assert.Equal(0, conv3(null)); 44 | Assert.Equal(10, conv3(10)); 45 | 46 | var conv4 = GenerateConverter(provider); 47 | Assert.Null(conv4(null)); 48 | Assert.Equal((byte)10, conv4(10)); 49 | Assert.Null(conv4(300)); 50 | 51 | 52 | 53 | var conv5 = GenerateConverter(provider); 54 | Assert.Equal(0, conv5(TestEnum.A)); 55 | Assert.Equal(1, conv5(TestEnum.B)); 56 | 57 | var conv6 = GenerateConverter(provider); 58 | Assert.Equal(TestEnum.B, conv6(1)); 59 | Assert.Equal((TestEnum)10, conv6(10)); 60 | 61 | var conv7 = GenerateConverter(provider); 62 | Assert.Equal(new DateTime(2010, 1, 1), conv7(new DateTime(2010, 1, 1))); 63 | 64 | // enum --> string 65 | var conv8 = GenerateConverter(provider); 66 | Assert.Equal("hello", conv8(TestEnum.A)); 67 | Assert.Equal("b", conv8(TestEnum.B)); 68 | Assert.Equal("10", conv8((TestEnum)10)); 69 | 70 | // string --> enum 71 | var conv9 = GenerateConverter(provider); 72 | Assert.Equal(TestEnum.A, conv9("hello")); 73 | Assert.Equal(TestEnum.B, conv9("b")); 74 | Assert.Equal((TestEnum)10, conv9("10")); 75 | Assert.Equal(TestEnum.A, conv9("11A")); 76 | 77 | var conv10 = GenerateConverter(provider); 78 | Assert.Equal(TestEnum.A, conv10("hello")); 79 | Assert.Equal(TestEnum.B, conv10("b")); 80 | Assert.Equal((TestEnum)10, conv10("10")); 81 | Assert.Null(conv10("11A")); 82 | 83 | } 84 | 85 | 86 | private Func GenerateConverter(IMapperProvider provider) 87 | { 88 | ParameterExpression s = Expression.Parameter(typeof(TSource)); 89 | ParameterExpression t = Expression.Variable(typeof(TTarget)); 90 | ParameterExpression checkerVar = Expression.Variable(typeof(CircularRefChecker)); 91 | PropertyAssignGenerator g = new PropertyAssignGenerator(s,t, checkerVar, provider); 92 | var expression = g.GenerateExpression(); 93 | 94 | Expression> lam = Expression.Lambda>( 95 | Expression.Block(new ParameterExpression[] { t, checkerVar }, 96 | Expression.Assign(checkerVar, Expression.New(typeof(CircularRefChecker))), 97 | expression, t), s); 98 | Func func = lam.Compile(); 99 | return func; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Extensions/HttpClient/HttpMessageExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using Xfrogcn.AspNetCore.Extensions; 4 | 5 | namespace System.Net.Http 6 | { 7 | public static class HttpMessageExtensions 8 | { 9 | public static JsonHelper JsonHelper = new JsonHelper(); 10 | 11 | /// 12 | /// 从Response中获取指定类型的对象 13 | /// 14 | /// 应答对象类型 15 | /// 应答消息 16 | /// 应答对象 17 | public static async Task GetObjectAsync(this HttpResponseMessage response, bool copy = false) 18 | { 19 | if (response == null || response.Content == null) 20 | { 21 | return default; 22 | } 23 | 24 | Stream stream = null; 25 | if (copy) 26 | { 27 | await response.Content.LoadIntoBufferAsync(); 28 | } 29 | 30 | stream = await response.Content.ReadAsStreamAsync(); 31 | 32 | 33 | if (typeof(TResponse) == typeof(string)) 34 | { 35 | StreamReader sr = new StreamReader(stream); 36 | var str = (TResponse)(object)(await sr.ReadToEndAsync()); 37 | stream.Position = 0; 38 | return str; 39 | } 40 | else if (typeof(TResponse) == typeof(HttpResponseMessage)) 41 | { 42 | // 如果类型为HttpResponseMessage直接返回 43 | return (TResponse)(object)response; 44 | } 45 | else 46 | { 47 | var obj = await JsonHelper.ToObjectAsync(stream); 48 | stream.Position = 0; 49 | return obj; 50 | } 51 | } 52 | 53 | public static async Task GetObjectAsync(this HttpRequestMessage request, bool copy = false) 54 | { 55 | if (request == null || request.Content == null) 56 | { 57 | return default; 58 | } 59 | 60 | Stream stream = null; 61 | if (copy) 62 | { 63 | await request.Content.LoadIntoBufferAsync(); 64 | } 65 | 66 | stream = await request.Content.ReadAsStreamAsync(); 67 | 68 | 69 | if (typeof(TResponse) == typeof(string)) 70 | { 71 | StreamReader sr = new StreamReader(stream); 72 | var str = (TResponse)(object)(await sr.ReadToEndAsync()); 73 | stream.Position = 0; 74 | return str; 75 | } 76 | else if (typeof(TResponse) == typeof(HttpRequestMessage)) 77 | { 78 | // 如果类型为HttpResponseMessage直接返回 79 | return (TResponse)(object)request; 80 | } 81 | else 82 | { 83 | var obj = await JsonHelper.ToObjectAsync(stream); 84 | stream.Position = 0; 85 | return obj; 86 | } 87 | } 88 | 89 | 90 | 91 | public static async Task WriteObjectAsync(this HttpRequestMessage request, object body) 92 | { 93 | MemoryStream ms = await createContentStream(body); 94 | request.Content = new StreamContent(ms); 95 | } 96 | 97 | public static async Task WriteObjectAsync(this HttpResponseMessage rsponse, object body) 98 | { 99 | MemoryStream ms = await createContentStream(body); 100 | rsponse.Content = new StreamContent(ms); 101 | } 102 | 103 | 104 | private static async Task createContentStream(object body) 105 | { 106 | MemoryStream ms = new MemoryStream(); 107 | 108 | if (body != null) 109 | { 110 | if (body.GetType() == typeof(string)) 111 | { 112 | StreamWriter sw = new StreamWriter(ms); 113 | await sw.WriteAsync((string)body); 114 | await sw.FlushAsync(); 115 | ms.Position = 0; 116 | } 117 | else 118 | { 119 | await JsonHelper.ToJsonAsync(ms, body); 120 | ms.Flush(); 121 | ms.Position = 0; 122 | } 123 | 124 | } 125 | 126 | return ms; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Extensions/HttpClient/LoggingDetailMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Xfrogcn.AspNetCore.Extensions 8 | { 9 | /// 10 | /// 消息日志Handler 11 | /// 12 | class LoggingDetailMessageHandler : DelegatingHandler 13 | { 14 | private readonly ILogger _logger; 15 | 16 | private static readonly EventId requestEventId = new EventId(500, "RequestDetail"); 17 | private static readonly EventId responseEventId = new EventId(501, "ResponseDetail"); 18 | private static readonly EventId requestErrorEventId = new EventId(505, "RequestError"); 19 | private static readonly Action _requestLog = LoggerMessage.Define(LogLevel.Trace, requestEventId, "请求方法:{httpMethod}, 请求URI:{uri}, 请求内容:{content}"); 20 | private static readonly Action _requestErrorLog = LoggerMessage.Define(LogLevel.Error, requestErrorEventId, "请求发生异常,请求方法:{httpMethod}, 请求URI:{uri}, 请求内容:{content}"); 21 | private static readonly Action _responseLog = LoggerMessage.Define(LogLevel.Trace, responseEventId, "请求方法:{httpMethod}, 请求URI:{uri}, 应答内容:{content}"); 22 | 23 | public LoggingDetailMessageHandler(ILogger logger) 24 | { 25 | _logger = logger; 26 | } 27 | 28 | 29 | private bool isTextContent(string mediaType) 30 | { 31 | if(!string.IsNullOrEmpty(mediaType) && (mediaType.Contains("json") || 32 | mediaType.Contains("xml") || 33 | mediaType.Contains("html") || 34 | mediaType.Contains("text")) 35 | ) 36 | { 37 | return true; 38 | } 39 | return false; 40 | } 41 | 42 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 43 | { 44 | 45 | string requestContent = null; 46 | 47 | if (_logger.IsEnabled(LogLevel.Trace)) 48 | { 49 | if (request.Method != HttpMethod.Get && request.Content != null) 50 | { 51 | if (isTextContent(request.Content.Headers?.ContentType?.MediaType)) 52 | { 53 | requestContent = await request.Content.ReadAsStringAsync(); 54 | } 55 | else 56 | { 57 | requestContent = $"stream content, length: {request.Content.Headers.ContentLength}"; 58 | } 59 | } 60 | if (string.IsNullOrEmpty(requestContent)) 61 | { 62 | requestContent = string.Empty; 63 | } 64 | _requestLog(_logger, request.Method, request.RequestUri, requestContent, null); 65 | } 66 | try 67 | { 68 | var response = await base.SendAsync(request, cancellationToken); 69 | 70 | 71 | if (_logger.IsEnabled(LogLevel.Trace)) 72 | { 73 | string responseContent = string.Empty; 74 | if (response != null && response.Content != null && isTextContent(response.Content.Headers?.ContentType?.MediaType) && (response.Content.Headers.ContentLength ?? 0) <= (1024 * 1024 * 5)) 75 | { 76 | responseContent = await response.Content.ReadAsStringAsync(); 77 | } 78 | 79 | if (response == null) 80 | { 81 | responseContent = "请求应答返回null"; 82 | } 83 | _responseLog(_logger, request.Method, request.RequestUri, responseContent, null); 84 | } 85 | return response; 86 | 87 | } 88 | catch (Exception e) 89 | { 90 | if (request.Method != HttpMethod.Get && requestContent==null) 91 | { 92 | requestContent = await request.Content.ReadAsStringAsync(); 93 | } 94 | _requestErrorLog(_logger, request.Method, request.RequestUri, requestContent, e); 95 | throw; 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Extensions/Logger/Json/CompactJsonFormatter.cs: -------------------------------------------------------------------------------- 1 | using Serilog.Events; 2 | using Serilog.Formatting; 3 | using Serilog.Formatting.Json; 4 | using Serilog.Parsing; 5 | using System; 6 | using System.IO; 7 | using System.Linq; 8 | 9 | namespace Xfrogcn.AspNetCore.Extensions.Logger.Json 10 | { 11 | class CompactJsonFormatter : ITextFormatter 12 | { 13 | readonly JsonValueFormatter _valueFormatter; 14 | readonly WebApiConfig _config; 15 | 16 | /// 17 | /// Construct a , optionally supplying a formatter for 18 | /// s on the event. 19 | /// 20 | /// A value formatter, or null. 21 | public CompactJsonFormatter(WebApiConfig config, JsonValueFormatter valueFormatter = null) 22 | { 23 | _valueFormatter = valueFormatter ?? new JsonValueFormatterEx(config, typeTagName: "$type"); 24 | _config = config; 25 | } 26 | 27 | /// 28 | /// Format the log event into the output. Subsequent events will be newline-delimited. 29 | /// 30 | /// The event to format. 31 | /// The output. 32 | public void Format(LogEvent logEvent, TextWriter output) 33 | { 34 | FormatEvent(_config, logEvent, output, _valueFormatter); 35 | output.WriteLine(); 36 | } 37 | 38 | /// 39 | /// Format the log event into the output. 40 | /// 41 | /// The event to format. 42 | /// The output. 43 | /// A value formatter for s on the event. 44 | public static void FormatEvent(WebApiConfig config, LogEvent logEvent, TextWriter output, JsonValueFormatter valueFormatter) 45 | { 46 | if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); 47 | if (output == null) throw new ArgumentNullException(nameof(output)); 48 | if (valueFormatter == null) throw new ArgumentNullException(nameof(valueFormatter)); 49 | 50 | output.Write("{\"@t\":\""); 51 | output.Write(logEvent.Timestamp.UtcDateTime.ToString("O")); 52 | output.Write("\",\"@mt\":"); 53 | JsonValueFormatter.WriteQuotedJsonString(JsonValueFormatterEx.TrimString(config, logEvent.MessageTemplate.Text), output); 54 | 55 | 56 | var tokensWithFormat = logEvent.MessageTemplate.Tokens 57 | .OfType() 58 | .Where(pt => pt.Format != null); 59 | 60 | // Better not to allocate an array in the 99.9% of cases where this is false 61 | // ReSharper disable once PossibleMultipleEnumeration 62 | if (tokensWithFormat.Any()) 63 | { 64 | output.Write(",\"@r\":["); 65 | var delim = ""; 66 | foreach (var r in tokensWithFormat) 67 | { 68 | output.Write(delim); 69 | delim = ","; 70 | var space = new StringWriter(); 71 | r.Render(logEvent.Properties, space); 72 | JsonValueFormatter.WriteQuotedJsonString(space.ToString(), output); 73 | } 74 | output.Write(']'); 75 | } 76 | 77 | if (logEvent.Level != LogEventLevel.Information) 78 | { 79 | output.Write(",\"@l\":\""); 80 | output.Write(logEvent.Level); 81 | output.Write('\"'); 82 | } 83 | 84 | if (logEvent.Exception != null) 85 | { 86 | output.Write(",\"@x\":"); 87 | JsonValueFormatter.WriteQuotedJsonString(logEvent.Exception.ToString(), output); 88 | } 89 | 90 | foreach (var property in logEvent.Properties) 91 | { 92 | var name = property.Key; 93 | if (name.Length > 0 && name[0] == '@') 94 | { 95 | // Escape first '@' by doubling 96 | name = '@' + name; 97 | } 98 | 99 | output.Write(','); 100 | JsonValueFormatter.WriteQuotedJsonString(name, output); 101 | output.Write(':'); 102 | valueFormatter.Format(property.Value, output); 103 | } 104 | 105 | output.Write('}'); 106 | } 107 | 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Extensions/HttpClient/MockHttpMessageHandlerOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Xfrogcn.AspNetCore.Extensions 10 | { 11 | public class MockHttpMessageHandlerOptions 12 | { 13 | internal class MockItem 14 | { 15 | /// 16 | /// 先决条件 17 | /// 18 | public Func Predicate { get; set; } 19 | /// 20 | /// 处理 21 | /// 22 | public Func Proc { get; set; } 23 | } 24 | 25 | private readonly List _mockList = new List(); 26 | 27 | private readonly JsonHelper _jsonHelper = new JsonHelper(); 28 | 29 | 30 | internal IReadOnlyList MockList => _mockList; 31 | 32 | 33 | private Func _urlPredicate(string url, HttpMethod method) 34 | { 35 | Func _predicate = (msg) => 36 | { 37 | if( url == "*") 38 | { 39 | return true; 40 | } 41 | return msg.RequestUri.AbsolutePath.ToLower() == url.ToLower() && msg.Method == method; 42 | }; 43 | return _predicate; 44 | } 45 | 46 | 47 | public MockHttpMessageHandlerOptions AddMock(string url, HttpMethod method, Func proc) 48 | { 49 | Func p = _urlPredicate(url, method); 50 | return AddMock(p, proc); 51 | } 52 | 53 | 54 | public MockHttpMessageHandlerOptions AddMock(string url, HttpMethod method, HttpStatusCode statusCode) 55 | { 56 | Func p = _urlPredicate(url, method); 57 | Func proc = (r, response) => 58 | { 59 | response.StatusCode = statusCode; 60 | return Task.CompletedTask; 61 | }; 62 | return AddMock(p, proc); 63 | 64 | } 65 | 66 | public MockHttpMessageHandlerOptions AddMock(string url, HttpMethod method, Exception exception) 67 | { 68 | Func p = _urlPredicate(url, method); 69 | Func proc = (r, response) => 70 | { 71 | throw exception; 72 | }; 73 | return AddMock(p, proc); 74 | } 75 | 76 | public MockHttpMessageHandlerOptions AddMock(string url, HttpMethod method, TObject obj, HttpStatusCode statusCode = HttpStatusCode.OK) 77 | { 78 | Func p = _urlPredicate(url, method); 79 | Func proc = (r, response) => 80 | { 81 | response.StatusCode = statusCode; 82 | response.Content = new StringContent(obj == null ? "" : _jsonHelper.ToJson(obj)); 83 | return Task.CompletedTask; 84 | }; 85 | return AddMock(p, proc); 86 | } 87 | 88 | public MockHttpMessageHandlerOptions AddMock(string url, HttpMethod method, string str, HttpStatusCode statusCode = HttpStatusCode.OK) 89 | { 90 | Func p = _urlPredicate(url, method); 91 | Func proc = (r, response) => 92 | { 93 | response.StatusCode = statusCode; 94 | response.Content = new StringContent(str); 95 | return Task.CompletedTask; 96 | }; 97 | return AddMock(p, proc); 98 | } 99 | 100 | 101 | 102 | public MockHttpMessageHandlerOptions AddMock(Func predicate, Func proc) 103 | { 104 | if( predicate == null) 105 | { 106 | throw new ArgumentNullException(nameof(predicate)); 107 | } 108 | if( proc == null) 109 | { 110 | throw new ArgumentNullException(nameof(proc)); 111 | } 112 | MockItem mi = new MockItem() 113 | { 114 | Predicate = predicate, 115 | Proc = proc 116 | }; 117 | _mockList.Add(mi); 118 | return this; 119 | } 120 | 121 | } 122 | } 123 | --------------------------------------------------------------------------------