refactor: Move some of the dependency injection code to the Common Library.

This commit is contained in:
real-zony
2022-10-06 12:45:01 +08:00
parent aa90e232f7
commit ecab0e0f5c
34 changed files with 60 additions and 73 deletions

View File

@@ -0,0 +1,76 @@
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using ZonyLrcTools.Common.Infrastructure.Extensions;
namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
{
public static class AutoDependencyInjectExtensions
{
/// <summary>
/// 开始进行自动依赖注入。
/// </summary>
/// <remarks>
/// 会根据实现了 <see cref="ITransientDependency"/> 或 <see cref="ISingletonDependency"/> 的接口进行自动注入。
/// </remarks>
/// <param name="services">服务定义集合。</param>
/// <typeparam name="TAssemblyType">需要注入的任意类型。</typeparam>
public static IServiceCollection BeginAutoDependencyInject<TAssemblyType>(this IServiceCollection services)
{
var allTypes = typeof(TAssemblyType).Assembly
.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType)
.ToArray();
var transientTypes = allTypes.Where(t => typeof(ITransientDependency).IsAssignableFrom(t));
var singletonTypes = allTypes.Where(t => typeof(ISingletonDependency).IsAssignableFrom(t));
transientTypes.Foreach(t =>
{
foreach (var exposedService in GetDefaultExposedTypes(t))
{
services.Add(CreateServiceDescriptor(t, exposedService, ServiceLifetime.Transient));
}
});
singletonTypes.Foreach(t =>
{
foreach (var exposedService in GetDefaultExposedTypes(t))
{
services.Add(CreateServiceDescriptor(t, exposedService, ServiceLifetime.Singleton));
}
});
return services;
}
public static List<Type> GetDefaultExposedTypes(Type type)
{
var serviceTypes = new List<Type>();
foreach (var interfaceType in type.GetTypeInfo().GetInterfaces())
{
var interfaceName = interfaceType.Name;
if (interfaceName.StartsWith("I"))
{
interfaceName = interfaceName.Substring(1, interfaceName.Length - 1);
}
if (type.Name.EndsWith(interfaceName))
{
serviceTypes.Add(interfaceType);
serviceTypes.Add(type);
}
}
return serviceTypes;
}
public static ServiceDescriptor CreateServiceDescriptor(Type implementationType,
Type exposingServiceType,
ServiceLifetime lifetime)
{
return new ServiceDescriptor(exposingServiceType, implementationType, lifetime);
}
}
}

View File

@@ -0,0 +1,9 @@
namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
{
/// <summary>
/// 继承了本接口的类都会以单例的形式注入到 IoC 容器当中。
/// </summary>
public interface ISingletonDependency
{
}
}

View File

@@ -0,0 +1,9 @@
namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
{
/// <summary>
/// 继承了本接口的类都会以瞬时的形式注入到 IoC 容器当中。
/// </summary>
public interface ITransientDependency
{
}
}

View File

@@ -0,0 +1,55 @@
using System.Net;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using ZonyLrcTools.Common.Configuration;
using ZonyLrcTools.Common.Infrastructure.Network;
namespace ZonyLrcTools.Common.Infrastructure.DependencyInject
{
/// <summary>
/// Service 注入的扩展方法。
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// 配置工具会用到的服务。
/// </summary>
public static IServiceCollection ConfigureToolService(this IServiceCollection services)
{
if (services == null)
{
return null;
}
services.AddHttpClient(DefaultWarpHttpClient.HttpClientNameConstant)
.ConfigurePrimaryHttpMessageHandler(provider =>
{
var option = provider.GetRequiredService<IOptions<GlobalOptions>>().Value;
return new HttpClientHandler
{
UseProxy = option.NetworkOptions.IsEnable,
Proxy = new WebProxy($"{option.NetworkOptions.Ip}:{option.NetworkOptions.Port}")
};
});
return services;
}
/// <summary>
/// 配置工具关联的配置信息(<see cref="IConfiguration"/>)。
/// </summary>
public static IServiceCollection ConfigureConfiguration(this IServiceCollection services)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddYamlFile("config.yaml")
.Build();
services.Configure<GlobalOptions>(configuration.GetSection("globalOption"));
return services;
}
}
}

View File

@@ -0,0 +1,24 @@
namespace ZonyLrcTools.Common.Infrastructure.Exceptions
{
/// <summary>
/// 带错误码的异常实现。
/// </summary>
public class ErrorCodeException : Exception
{
public int ErrorCode { get; }
public object AttachObject { get; }
/// <summary>
/// 构建一个新的 <see cref="ErrorCodeException"/> 对象。
/// </summary>
/// <param name="errorCode">错误码,参考 <see cref="ErrorCodes"/> 类的定义。</param>
/// <param name="message">错误信息。</param>
/// <param name="attachObj">附加的对象数据。</param>
public ErrorCodeException(int errorCode, string message = null, object attachObj = null) : base(message)
{
ErrorCode = errorCode;
AttachObject = attachObj;
}
}
}

View File

@@ -0,0 +1,41 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace ZonyLrcTools.Common.Infrastructure.Exceptions
{
/// <summary>
/// 错误码相关的帮助类。
/// </summary>
public static class ErrorCodeHelper
{
public static Dictionary<int, string> ErrorMessages { get; }
static ErrorCodeHelper()
{
ErrorMessages = new Dictionary<int, string>();
}
/// <summary>
/// 从 err_msg.json 文件加载错误信息。
/// </summary>
public static void LoadErrorMessage()
{
// 防止重复加载。
if (ErrorMessages.Count != 0)
{
return;
}
var jsonPath = Path.Combine(Directory.GetCurrentDirectory(), "Resources", "error_msg.json");
using var jsonReader = new JsonTextReader(File.OpenText(jsonPath));
var jsonObj = JObject.Load(jsonReader);
var errors = jsonObj.SelectTokens("$.Error.*");
var warnings = jsonObj.SelectTokens("$.Warning.*");
errors.Union(warnings).Select(m => m.Parent).OfType<JProperty>().ToList()
.ForEach(m => ErrorMessages.Add(int.Parse(m.Name), m.Value.Value<string>()));
}
public static string GetMessage(int errorCode) => ErrorMessages[errorCode];
}
}

View File

@@ -0,0 +1,96 @@
namespace ZonyLrcTools.Common.Infrastructure.Exceptions
{
/// <summary>
/// 错误码。
/// </summary>
public static class ErrorCodes
{
#region > <
/// <summary>
/// 文本: 待搜索的后缀不能为空。
/// </summary>
public const int FileSuffixIsEmpty = 10001;
/// <summary>
/// 文本: 需要扫描的目录不存在,请确认路径是否正确。。
/// </summary>
public const int DirectoryNotExist = 10002;
/// <summary>
/// 文本: 不能获取文件的后缀信息。
/// </summary>
public const int UnableToGetTheFileExtension = 10003;
/// <summary>
/// 文本: 没有扫描到任何音乐文件。
/// </summary>
public const int NoFilesWereScanned = 10004;
/// <summary>
/// 文本: 指定的编码不受支持,请检查配置,所有受支持的编码名称。
/// </summary>
public const int NotSupportedFileEncoding = 10005;
#endregion
#region > <
/// <summary>
/// 文本: 扫描文件时出现了错误。
/// </summary>
public const int ScanFileError = 50001;
/// <summary>
/// 文本: 歌曲名称或歌手名称均为空,无法进行搜索。
/// </summary>
public const int SongNameAndArtistIsNull = 50002;
/// <summary>
/// 文本: 歌曲名称不能为空,无法进行搜索。
/// </summary>
public const int SongNameIsNull = 50003;
/// <summary>
/// 文本: 下载器没有搜索到对应的歌曲信息。
/// </summary>
public const int NoMatchingSong = 50004;
/// <summary>
/// 文本: 下载请求的返回值不合法,可能是服务端故障。
/// </summary>
public const int TheReturnValueIsIllegal = 50005;
/// <summary>
/// 文本: 标签信息读取器为空,无法解析音乐 Tag 信息。
/// </summary>
public const int LoadTagInfoProviderError = 50006;
/// <summary>
/// 文本: TagLib 标签读取器出现了预期之外的异常。
/// </summary>
public const int TagInfoProviderLoadInfoFailed = 50007;
/// <summary>
/// 文本: 服务接口限制,无法进行请求,请尝试使用代理服务器。
/// </summary>
public const int ServiceUnavailable = 50008;
/// <summary>
/// 文本: 对目标服务器执行 Http 请求失败。
/// </summary>
public const int HttpRequestFailed = 50009;
/// <summary>
/// 文本: Http 请求的结果反序列化为 Json 失败。
/// </summary>
public const int HttpResponseConvertJsonFailed = 50010;
/// <summary>
/// 文本: 目前仅支持 NCM 格式的歌曲转换操作。
/// </summary>
public const int OnlySupportNcmFormatFile = 50011;
#endregion
}
}

View File

@@ -0,0 +1,21 @@
namespace ZonyLrcTools.Common.Infrastructure.Extensions
{
/// <summary>
/// Linq 相关的扩展方法。
/// </summary>
public static class LinqHelper
{
/// <summary>
/// 使用 Lambda 的形式遍历指定的迭代器。
/// </summary>
/// <param name="items">等待遍历的迭代器实例。</param>
/// <param name="action">遍历时需要执行的操作。</param>
public static void Foreach<T>(this IEnumerable<T> items, Action<T> action)
{
foreach (var item in items)
{
action(item);
}
}
}
}

View File

@@ -0,0 +1,151 @@
using System.Net;
using System.Reflection;
using System.Text;
using Newtonsoft.Json;
using ZonyLrcTools.Common.Infrastructure.DependencyInject;
using ZonyLrcTools.Common.Infrastructure.Exceptions;
namespace ZonyLrcTools.Common.Infrastructure.Network
{
public class DefaultWarpHttpClient : IWarpHttpClient, ITransientDependency
{
private readonly IHttpClientFactory _httpClientFactory;
public const string HttpClientNameConstant = "WarpClient";
public DefaultWarpHttpClient(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async ValueTask<string> PostAsync(string url,
object parameters = null,
bool isQueryStringParam = false,
Action<HttpRequestMessage> requestOption = null)
{
var parametersStr = isQueryStringParam ? BuildQueryString(parameters) : BuildJsonBodyString(parameters);
var requestMessage = new HttpRequestMessage(HttpMethod.Post, new Uri(url));
requestMessage.Content = new StringContent(parametersStr);
requestOption?.Invoke(requestMessage);
using var responseMessage = await BuildHttpClient().SendAsync(requestMessage);
var responseContentString = await responseMessage.Content.ReadAsStringAsync();
return ValidateHttpResponse(responseMessage, parameters, responseContentString);
}
public async ValueTask<TResponse> PostAsync<TResponse>(string url,
object parameters = null,
bool isQueryStringParam = false,
Action<HttpRequestMessage> requestOption = null)
{
var responseString = await PostAsync(url, parameters, isQueryStringParam, requestOption);
return ConvertHttpResponseToObject<TResponse>(parameters, responseString);
}
public async ValueTask<string> GetAsync(string url,
object parameters = null,
Action<HttpRequestMessage> requestOption = null)
{
var requestParamsStr = BuildQueryString(parameters);
var requestMsg = new HttpRequestMessage(HttpMethod.Get, new Uri($"{url}?{requestParamsStr}"));
requestOption?.Invoke(requestMsg);
using var responseMessage = await BuildHttpClient().SendAsync(requestMsg);
var responseContentString = await responseMessage.Content.ReadAsStringAsync();
return ValidateHttpResponse(responseMessage, parameters, responseContentString);
}
public async ValueTask<TResponse> GetAsync<TResponse>(string url,
object parameters = null,
Action<HttpRequestMessage> requestOption = null)
{
var responseString = await GetAsync(url, parameters, requestOption);
return ConvertHttpResponseToObject<TResponse>(parameters, responseString);
}
protected virtual HttpClient BuildHttpClient()
{
return _httpClientFactory.CreateClient(HttpClientNameConstant);
}
private string BuildQueryString(object parameters)
{
if (parameters == null)
{
return string.Empty;
}
var type = parameters.GetType();
if (type == typeof(string))
{
return parameters as string;
}
var properties = type.GetProperties();
var paramBuilder = new StringBuilder();
foreach (var propertyInfo in properties)
{
var jsonProperty = propertyInfo.GetCustomAttribute<JsonPropertyAttribute>();
var propertyName = jsonProperty != null ? jsonProperty.PropertyName : propertyInfo.Name;
paramBuilder.Append($"{propertyName}={propertyInfo.GetValue(parameters)}&");
}
return paramBuilder.ToString().TrimEnd('&');
}
private string BuildJsonBodyString(object parameters)
{
if (parameters == null) return string.Empty;
if (parameters is string result) return result;
return JsonConvert.SerializeObject(parameters);
}
/// <summary>
/// 校验 Http 响应 <see cref="HttpResponseMessage"/> 是否合法。
/// </summary>
/// <param name="responseMessage">执行 Http 请求之后的响应实例。</param>
/// <param name="requestParameters">执行 Http 请求时传递的参数。</param>
/// <param name="responseString">执行 Http 请求之后响应内容。</param>
/// <returns>如果响应正常,则返回具体的响应内容。</returns>
/// <exception cref="ErrorCodeException">如果 Http 响应不正常,则可能抛出本异常。</exception>
private string ValidateHttpResponse(HttpResponseMessage responseMessage, object requestParameters, string responseString)
{
return responseMessage.StatusCode switch
{
HttpStatusCode.OK => responseString,
HttpStatusCode.ServiceUnavailable => throw new ErrorCodeException(ErrorCodes.ServiceUnavailable),
_ => throw new ErrorCodeException(ErrorCodes.HttpRequestFailed, attachObj: new { requestParameters, responseString })
};
}
/// <summary>
/// 将 Http 响应的字符串反序列化为指定类型 <see cref="TResponse"/> 的对象。
/// </summary>
/// <param name="requestParameters">执行 Http 请求时传递的参数。</param>
/// <param name="responseString">执行 Http 请求之后响应内容。</param>
/// <typeparam name="TResponse">需要将响应结果反序列化的目标类型。</typeparam>
/// <exception cref="ErrorCodeException">如果反序列化失败,则可能抛出本异常。</exception>
private TResponse ConvertHttpResponseToObject<TResponse>(object requestParameters, string responseString)
{
var throwException = new ErrorCodeException(ErrorCodes.HttpResponseConvertJsonFailed, attachObj: new { requestParameters, responseString });
try
{
var responseObj = JsonConvert.DeserializeObject<TResponse>(responseString);
if (responseObj != null) return responseObj;
throw throwException;
}
catch (JsonSerializationException)
{
throw throwException;
}
}
}
}

View File

@@ -0,0 +1,59 @@
namespace ZonyLrcTools.Common.Infrastructure.Network
{
/// <summary>
/// 基于 <see cref="IHttpClientFactory"/> 封装的 HTTP 请求客户端。
/// </summary>
public interface IWarpHttpClient
{
/// <summary>
/// 根据指定的配置执行 POST 请求,并以 <see cref="string"/> 作为返回值。
/// </summary>
/// <param name="url">请求的 URL 地址。</param>
/// <param name="parameters">请求的参数。</param>
/// <param name="isQueryStringParam">是否以 QueryString 形式携带参数。</param>
/// <param name="requestOption">请求时的配置动作。</param>
/// <returns>服务端的响应结果。</returns>
ValueTask<string> PostAsync(string url,
object parameters = null,
bool isQueryStringParam = false,
Action<HttpRequestMessage> requestOption = null);
/// <summary>
/// 根据指定的配置执行 POST 请求,并将结果反序列化为 <see cref="TResponse"/> 对象。
/// </summary>
/// <param name="url">请求的 URL 地址。</param>
/// <param name="parameters">请求的参数。</param>
/// <param name="isQueryStringParam">是否以 QueryString 形式携带参数。</param>
/// <param name="requestOption">请求时的配置动作。</param>
/// <typeparam name="TResponse">需要将响应结果反序列化的目标类型。</typeparam>
/// <returns>服务端的响应结果。</returns>
ValueTask<TResponse> PostAsync<TResponse>(string url,
object parameters = null,
bool isQueryStringParam = false,
Action<HttpRequestMessage> requestOption = null);
/// <summary>
/// 根据指定的配置执行 GET 请求,并以 <see cref="string"/> 作为返回值。
/// </summary>
/// <param name="url">请求的 URL 地址。</param>
/// <param name="parameters">请求的参数。</param>
/// <param name="requestOption">请求时的配置动作。</param>
/// <returns>服务端的响应结果。</returns>
ValueTask<string> GetAsync(string url,
object parameters = null,
Action<HttpRequestMessage> requestOption = null);
/// <summary>
/// 根据指定的配置执行 GET 请求,并将结果反序列化为 <see cref="TResponse"/> 对象。
/// </summary>
/// <param name="url">请求的 URL 地址。</param>
/// <param name="parameters">请求的参数。</param>
/// <param name="requestOption">请求时的配置动作。</param>
/// <typeparam name="TResponse">需要将响应结果反序列化的目标类型。</typeparam>
/// <returns>服务端的响应结果。</returns>
ValueTask<TResponse> GetAsync<TResponse>(
string url,
object parameters = null,
Action<HttpRequestMessage> requestOption = null);
}
}