前言
想變優秀的第N天。
學習張老師的Blog.Core。
1.建立Asp.NetCore API
1.1建立專案
啟用OpenAPI:sawgger
不適用頂級語句:使用main函式
使用控制器:controller
1.2配置說明
iisSettings:iis配置。
http:kestrl啟動配置。
IIS Express:iis啟動配置。
2.倉儲+服務
建立以下公共類庫和API,他們分別是:
Peng.Net8:WebApi。
Peng.Net8.Common:公共幫助類。
Peng.Net8.Model:實體層。
Peng.Net8.Repository:倉儲層。
Peng.Net8.IService:服務介面層。
Peng.Net8.Service:服務層。
3.泛型基類
3.1倉儲基類
IBaseRepository:需要對傳入TEntity(實體模型)進行資料操作。
BaseRepository:實現IBaseRepository。
3.2服務基類
IBaseServices:對傳入的TEntity(實體模型)進行操作,但是不能返回TEntity,需要返回TVo(檢視模型),不能將實體欄位暴露給WebAPI層。
BaseServices:實現IBaseServices。
4.AutoMapper(物件對映)
使用AutoMapper是為了將檢視模型和實體模型進行相互轉換。
4.1安裝
在Peng.Net8.Common層安裝AutoMapper的倆個包,這裡直接貼上儲存就能自動安裝。
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
4.2使用
將UserInfo和UserInfoVo的欄位進行配置。
AutoMapperConfig:
/// <summary>
/// 靜態全域性 AutoMapper 配置檔案
/// </summary>
public class AutoMapperConfig
{
public static MapperConfiguration RegisterMappings()
{
return new MapperConfiguration(cfg =>
{
cfg.AddProfile(new CustomProfile());
});
}
}
注入:
builder.Services.AddAutoMapper(typeof(AutoMapperConfig));
AutoMapperConfig.RegisterMappings();
5.原生依賴注入
在NetCore開發中,減少new的使用,儘量使用依賴注入。(不應該使用new,比如複雜的鏈路、GC回收、記憶體洩露等)
AddSington:單例模式。
AddScope:會話模式。一個http請求。
AddTrasient:瞬時模式。
5.1倉儲服務程式碼
倉儲層:
服務層:
5.2依賴注入
注入:
6.自定義專案框架模版
官網地址:https://learn.microsoft.com/zh-cn/dotnet/core/tools/custom-templates
6.1template.json
{
"$schema": "https://json.schemastore.org/template.json",
"author": "peng", // 模板作者 必須
"classifications": [ "Web/WebAPI" ], //必須,這個對應模板的Tags 模板特徵標識。上文舉例的配置是因為我自定義的模板包括了console和webapi
"name": "Peng.Net8 Dotnet", //必須,這個對應模板的Templates 使用者看到的模板名稱
"identity": "Peng.Net8.Template", //可選,模板的唯一名稱
"shortName": "PNetTpl", //必須,這個對應模板的Short Name 短名稱。當使用CLI命令建立模板專案時,使用短名稱將利於使用。
"tags": {
"language": "C#",
"type": "project"
},
"sourceName": "Peng.Net8", // 可選,要替換的名字
"preferNameDirectory": true // 可選,新增目錄
}
6.2安裝模板
安裝模板 (絕對路徑)
dotnet new install 絕對路徑\Peng.Net8 --force
如果需要解除安裝重來的話,解除安裝模版命令
dotnet new uninstall 絕對路徑\Peng.Net8
檢視命令
donet new list
檢視模版支援選項,使用的名稱是template.json中的shortName
dotnet new 名稱 -h
6.3新模版建立專案
使用新模版建立專案(記得關閉vs,要不會報錯The process cannot access the file ‘ ’ because it is being used by another process.)。
dotnet new 模版名稱 -n 專案名稱
dotnet new PNetTpl -n PengPeng.Net8
- -n:專案名稱
- -o:生成專案路徑
- -E:/--EnableFramework 自定義命令 (生成專案模式)
建立成功
VS2022建立
6.4nuget
下載nuget.exe檔案
https://www.nuget.org/downloads
打包模板,並生成.nupkg檔案
nuget.exe pack Peng.Net8/peng.net8.template.nuspec
釋出到nuget
nuget push Peng.Net8.Template.1.0.0.nupkg -Source "你的nuget 服務 url" -ApiKey "你的nuget api key"
7.Autofac
7.1安裝Autofac
<ItemGroup>
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Autofac.Extras.DynamicProxy" Version="7.1.0" />
</ItemGroup>
7.2注入Autofac
建構函式注入
public class AutofacModuleRegister : Autofac.Module
{
/*
1、看是哪個容器起的作用,報錯是什麼
2、三步走匯入autofac容器
3、生命週期,hashcode對比,為什麼controller裡沒變化
4、屬性注入
*/
protected override void Load(ContainerBuilder builder)
{
var basePath = AppContext.BaseDirectory;
var servicesDllFile = Path.Combine(basePath, "Peng.Net8.Service.dll");
var repositoryDllFile = Path.Combine(basePath, "Peng.Net8.Repository.dll");
builder.RegisterGeneric(typeof(BaseRepository<>)).As(typeof(IBaseRepository<>)).InstancePerDependency(); //註冊倉儲
builder.RegisterGeneric(typeof(BaseServices<,>)).As(typeof(IBaseServices<,>)).InstancePerDependency(); //註冊服務
// 獲取 Service.dll 程式集服務,並註冊
var assemblysServices = Assembly.LoadFrom(servicesDllFile);
builder.RegisterAssemblyTypes(assemblysServices)
.AsImplementedInterfaces()
.InstancePerDependency()
.PropertiesAutowired();
// 獲取 Repository.dll 程式集服務,並註冊
var assemblysRepository = Assembly.LoadFrom(repositoryDllFile);
builder.RegisterAssemblyTypes(assemblysRepository)
.AsImplementedInterfaces()
.PropertiesAutowired()
.InstancePerDependency();
}
}
屬性注入:
public class AutofacPropertityModuleReg : Module
{
protected override void Load(ContainerBuilder builder)
{
var controllerBaseType = typeof(ControllerBase);
builder.RegisterAssemblyTypes(typeof(Program).Assembly)
.Where(t => controllerBaseType.IsAssignableFrom(t) && t != controllerBaseType)
.PropertiesAutowired();
}
}
把autofac注入到ServiceCollection
var builder = WebApplication.CreateBuilder(args);
builder.Host
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureContainer<ContainerBuilder>(builder =>
{
builder.RegisterModule<AutofacModuleRegister>();
builder.RegisterModule<AutofacPropertityModuleReg>();
});
// 屬性注入
builder.Services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());
// Add services to the container.
builder.Services.AddControllers();
7.3建構函式注入
在建構函式中賦值:
7.4屬性注入
ASP.NET Core預設不使用DI獲取Controller,是因為DI容器構建完成後就不能變更了,但是Controller是可能有動態載入的需求的。
需要使用IControllerActivator開啟Controller的屬性注入,預設不開啟。
必須使用public修飾屬性
8.AOP(Log)
8.1安裝
<PackageReference Include="Autofac.Extras.DynamicProxy" Version="7.1.0" />
8.2動態代理實現ServiceAOP
實現IInterceptor,執行完成後打出日誌
public class AOPLogInfo
{
/// <summary>
/// 請求時間
/// </summary>
public string RequestTime { get; set; } = string.Empty;
/// <summary>
/// 操作人員
/// </summary>
public string OpUserName { get; set; } = string.Empty;
/// <summary>
/// 請求方法名
/// </summary>
public string RequestMethodName { get; set; } = string.Empty;
/// <summary>
/// 請求引數名
/// </summary>
public string RequestParamsName { get; set; } = string.Empty;
/// <summary>
/// 請求引數資料JSON
/// </summary>
public string RequestParamsData { get; set; } = string.Empty;
/// <summary>
/// 請求響應間隔時間
/// </summary>
public string ResponseIntervalTime { get; set; } = string.Empty;
/// <summary>
/// 響應時間
/// </summary>
public string ResponseTime { get; set; } = string.Empty;
/// <summary>
/// 響應結果
/// </summary>
public string ResponseJsonData { get; set; } = string.Empty;
}
/// <summary>
/// 攔截器AOP 繼承IInterceptor介面
/// </summary>
public class ServiceAOP : IInterceptor
{
/// <summary>
/// 例項化IInterceptor唯一方法
/// </summary>
/// <param name="invocation">包含被攔截方法的資訊</param>
public void Intercept(IInvocation invocation)
{
string json;
try
{
json = JsonConvert.SerializeObject(invocation.Arguments);
}
catch (Exception ex)
{
json = "無法序列化,可能是蘭姆達表示式等原因造成,按照框架最佳化程式碼" + ex.ToString();
}
DateTime startTime = DateTime.Now;
AOPLogInfo apiLogAopInfo = new AOPLogInfo
{
RequestTime = startTime.ToString("yyyy-MM-dd hh:mm:ss fff"),
OpUserName = "",
RequestMethodName = invocation.Method.Name,
RequestParamsName = string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray()),
ResponseJsonData = json
};
try
{
//在被攔截的方法執行完畢後 繼續執行當前方法,注意是被攔截的是非同步的
invocation.Proceed();
// 非同步獲取異常,先執行
if (IsAsyncMethod(invocation.Method))
{
//Wait task execution and modify return value
if (invocation.Method.ReturnType == typeof(Task))
{
invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithPostActionAndFinally(
(Task)invocation.ReturnValue,
async () => await SuccessAction(invocation, apiLogAopInfo, startTime), /*成功時執行*/
ex =>
{
LogEx(ex, apiLogAopInfo);
});
}
//Task<TResult>
else
{
invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithPostActionAndFinallyAndGetResult(
invocation.Method.ReturnType.GenericTypeArguments[0],
invocation.ReturnValue,
async (o) => await SuccessAction(invocation, apiLogAopInfo, startTime, o), /*成功時執行*/
ex =>
{
LogEx(ex, apiLogAopInfo);
});
}
}
else
{
// 同步1
string jsonResult;
try
{
jsonResult = JsonConvert.SerializeObject(invocation.ReturnValue);
}
catch (Exception ex)
{
jsonResult = "無法序列化,可能是蘭姆達表示式等原因造成,按照框架最佳化程式碼" + ex.ToString();
}
DateTime endTime = DateTime.Now;
string ResponseTime = (endTime - startTime).Milliseconds.ToString();
apiLogAopInfo.ResponseTime = endTime.ToString("yyyy-MM-dd hh:mm:ss fff");
apiLogAopInfo.ResponseIntervalTime = ResponseTime + "ms";
apiLogAopInfo.ResponseJsonData = jsonResult;
Console.WriteLine(JsonConvert.SerializeObject(apiLogAopInfo));
}
}
catch (Exception ex)
{
LogEx(ex, apiLogAopInfo);
throw;
}
}
private async Task SuccessAction(IInvocation invocation, AOPLogInfo apiLogAopInfo, DateTime startTime, object o = null)
{
DateTime endTime = DateTime.Now;
string ResponseTime = (endTime - startTime).Milliseconds.ToString();
apiLogAopInfo.ResponseTime = endTime.ToString("yyyy-MM-dd hh:mm:ss fff");
apiLogAopInfo.ResponseIntervalTime = ResponseTime + "ms";
apiLogAopInfo.ResponseJsonData = JsonConvert.SerializeObject(o);
await Task.Run(() =>
{
Console.WriteLine("執行成功-->" + JsonConvert.SerializeObject(apiLogAopInfo));
});
}
private void LogEx(Exception ex, AOPLogInfo dataIntercept)
{
if (ex != null)
{
Console.WriteLine("error!!!:" + ex.Message + JsonConvert.SerializeObject(dataIntercept));
}
}
public static bool IsAsyncMethod(MethodInfo method)
{
return
method.ReturnType == typeof(Task) ||
method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>);
}
}
internal static class InternalAsyncHelper
{
public static async Task AwaitTaskWithPostActionAndFinally(Task actualReturnValue, Func<Task> postAction, Action<Exception> finalAction)
{
Exception exception = null;
try
{
await actualReturnValue;
await postAction();
}
catch (Exception ex)
{
exception = ex;
}
finally
{
finalAction(exception);
}
}
public static async Task<T> AwaitTaskWithPostActionAndFinallyAndGetResult<T>(Task<T> actualReturnValue, Func<object, Task> postAction,
Action<Exception> finalAction)
{
Exception exception = null;
try
{
var result = await actualReturnValue;
await postAction(result);
return result;
}
catch (Exception ex)
{
exception = ex;
throw;
}
finally
{
finalAction(exception);
}
}
public static object CallAwaitTaskWithPostActionAndFinallyAndGetResult(Type taskReturnType, object actualReturnValue,
Func<object, Task> action, Action<Exception> finalAction)
{
return typeof(InternalAsyncHelper)
.GetMethod("AwaitTaskWithPostActionAndFinallyAndGetResult", BindingFlags.Public | BindingFlags.Static)
.MakeGenericMethod(taskReturnType)
.Invoke(null, new object[] { actualReturnValue, action, finalAction });
}
}
8.3Autofac注入
只在服務層(Service)注入日誌。
var aopTypes = new List<Type>() { typeof(ServiceAOP) };
builder.RegisterType<ServiceAOP>();
builder.RegisterGeneric(typeof(BaseRepository<>)).As(typeof(IBaseRepository<>))
.InstancePerDependency(); //註冊倉儲
builder.RegisterGeneric(typeof(BaseServices<,>)).As(typeof(IBaseServices<,>))
.EnableInterfaceInterceptors()
.InterceptedBy(aopTypes.ToArray())
.InstancePerDependency(); //註冊服務
// 獲取 Service.dll 程式集服務,並註冊
var assemblysServices = Assembly.LoadFrom(servicesDllFile);
builder.RegisterAssemblyTypes(assemblysServices)
.AsImplementedInterfaces()
.InstancePerDependency()
.PropertiesAutowired()
.EnableInterfaceInterceptors()
.InterceptedBy(aopTypes.ToArray());
8.4實現
日誌成功打出。
9.Appseting單例類獲取配置
AspNetCore五大介面物件:
- ILogger:日誌。
- IServiceCollection:IOC。
- IOptions:選項。
- IConfiguration:配置。
- Middleware:中介軟體。
弊端:硬編碼,重構會有很大影響。比如大小寫。
9.1安裝
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
9.2實現AppSettings
public class AppSettings
{
public static IConfiguration Configuration { get; set; }
static string contentPath { get; set; }
public AppSettings(string contentPath)
{
string Path = "appsettings.json";
//如果你把配置檔案 是 根據環境變數來分開了,可以這樣寫
//Path = $"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json";
Configuration = new ConfigurationBuilder()
.SetBasePath(contentPath)
.Add(new JsonConfigurationSource
{
Path = Path,
Optional = false,
ReloadOnChange = true
}) //這樣的話,可以直接讀目錄裡的json檔案,而不是 bin 資料夾下的,所以不用修改複製屬性
.Build();
}
public AppSettings(IConfiguration configuration)
{
Configuration = configuration;
}
/// <summary>
/// 封裝要操作的字元
/// </summary>
/// <param name="sections">節點配置</param>
/// <returns></returns>
public static string app(params string[] sections)
{
try
{
if (sections.Any())
{
return Configuration[string.Join(":", sections)];
}
}
catch (Exception)
{
}
return "";
}
/// <summary>
/// 遞迴獲取配置資訊陣列
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sections"></param>
/// <returns></returns>
public static List<T> app<T>(params string[] sections)
{
List<T> list = new List<T>();
// 引用 Microsoft.Extensions.Configuration.Binder 包
Configuration.Bind(string.Join(":", sections), list);
return list;
}
/// <summary>
/// 根據路徑 configuration["App:Name"];
/// </summary>
/// <param name="sectionsPath"></param>
/// <returns></returns>
public static string GetValue(string sectionsPath)
{
try
{
return Configuration[sectionsPath];
}
catch (Exception)
{
}
return "";
}
}
9.3注入
builder.Services.AddSingleton(new AppSettings(builder.Configuration));
9.4使用
var redisEnable = AppSettings.app(new string[] { "Redis", "Enable" });
var redisConnectionString = AppSettings.GetValue("Redis:ConnectionString");
Console.WriteLine($"Enable: {redisEnable} , ConnectionString: {redisConnectionString}");
配置檔案:
10.IOptions
10.1安裝
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
10.2配置類
IConfigurableOptions是為了約束,只有繼承IConfigurableOptions才被注入到ServiceCollection中。
public interface IConfigurableOptions
{
}
/// <summary>
/// Redis快取配置選項
/// </summary>
public sealed class RedisOptions : IConfigurableOptions
{
/// <summary>
/// 是否啟用
/// </summary>
public bool Enable { get; set; }
/// <summary>
/// Redis連線
/// </summary>
public string ConnectionString { get; set; }
/// <summary>
/// 鍵值字首
/// </summary>
public string InstanceName { get; set; }
}
10.3實現
將實現IConfigurableOptions介面的配置類注入到ServiceCollection中。
public static class AllOptionRegister
{
public static void AddAllOptionRegister(this IServiceCollection services)
{
if (services == null) throw new ArgumentNullException(nameof(services));
foreach (var optionType in typeof(ConfigurableOptions).Assembly.GetTypes().Where(s =>
!s.IsInterface && typeof(IConfigurableOptions).IsAssignableFrom(s)))
{
services.AddConfigurableOptions(optionType);
}
}
}
public static class ConfigurableOptions
{
internal static IConfiguration Configuration;
public static void ConfigureApplication(this IConfiguration configuration)
{
Configuration = configuration;
}
/// <summary>新增選項配置</summary>
/// <typeparam name="TOptions">選項型別</typeparam>
/// <param name="services">服務集合</param>
/// <returns>服務集合</returns>
public static IServiceCollection AddConfigurableOptions<TOptions>(this IServiceCollection services)
where TOptions : class, IConfigurableOptions
{
Type optionsType = typeof(TOptions);
string path = GetConfigurationPath(optionsType);
services.Configure<TOptions>(Configuration.GetSection(path));
return services;
}
public static IServiceCollection AddConfigurableOptions(this IServiceCollection services, Type type)
{
string path = GetConfigurationPath(type);
var config = Configuration.GetSection(path);
Type iOptionsChangeTokenSource = typeof(IOptionsChangeTokenSource<>);
Type iConfigureOptions = typeof(IConfigureOptions<>);
Type configurationChangeTokenSource = typeof(ConfigurationChangeTokenSource<>);
Type namedConfigureFromConfigurationOptions = typeof(NamedConfigureFromConfigurationOptions<>);
iOptionsChangeTokenSource = iOptionsChangeTokenSource.MakeGenericType(type);
iConfigureOptions = iConfigureOptions.MakeGenericType(type);
configurationChangeTokenSource = configurationChangeTokenSource.MakeGenericType(type);
namedConfigureFromConfigurationOptions = namedConfigureFromConfigurationOptions.MakeGenericType(type);
services.AddOptions();
services.AddSingleton(iOptionsChangeTokenSource,
Activator.CreateInstance(configurationChangeTokenSource, Options.DefaultName, config) ?? throw new InvalidOperationException());
return services.AddSingleton(iConfigureOptions,
Activator.CreateInstance(namedConfigureFromConfigurationOptions, Options.DefaultName, config) ?? throw new InvalidOperationException());
}
/// <summary>獲取配置路徑</summary>
/// <param name="optionsType">選項型別</param>
/// <returns></returns>
public static string GetConfigurationPath(Type optionsType)
{
var endPath = new[] { "Option", "Options" };
var configurationPath = optionsType.Name;
foreach (var s in endPath)
{
if (configurationPath.EndsWith(s))
{
return configurationPath[..^s.Length];
}
}
return configurationPath;
}
}
10.4注入
這倆種方式都可以
var builder = WebApplication.CreateBuilder(args);
builder.Host
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureContainer<ContainerBuilder>(builder =>
{
builder.RegisterModule<AutofacModuleRegister>();
builder.RegisterModule<AutofacPropertityModuleReg>();
})
.ConfigureAppConfiguration((hostingContext, config) =>
{
hostingContext.Configuration.ConfigureApplication();
});
// 配置
//ConfigurableOptions.ConfigureApplication(builder.Configuration);
builder.Services.AddAllOptionRegister();
10.5使用
var redisOptions = _redisOptions.Value;
11.非依賴注入管道中獲取所有服務
11.1.安裝Serilog
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
11.2實現
RuntimeExtension:獲取專案程式集
public static class RuntimeExtension
{
/// <summary>
/// 獲取專案程式集,排除所有的系統程式集(Microsoft.***、System.***等)、Nuget下載包
/// </summary>
/// <returns></returns>
public static IList<Assembly> GetAllAssemblies()
{
var list = new List<Assembly>();
var deps = DependencyContext.Default;
//只載入專案中的程式集
var libs = deps.CompileLibraries.Where(lib => !lib.Serviceable && lib.Type == "project"); //排除所有的系統程式集、Nuget下載包
foreach (var lib in libs)
{
try
{
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(lib.Name));
list.Add(assembly);
}
catch (Exception e)
{
Log.Debug(e, "GetAllAssemblies Exception:{ex}", e.Message);
}
}
return list;
}
public static Assembly GetAssembly(string assemblyName)
{
return GetAllAssemblies().FirstOrDefault(assembly => assembly.FullName.Contains(assemblyName));
}
public static IList<Type> GetAllTypes()
{
var list = new List<Type>();
foreach (var assembly in GetAllAssemblies())
{
var typeInfos = assembly.DefinedTypes;
foreach (var typeInfo in typeInfos)
{
list.Add(typeInfo.AsType());
}
}
return list;
}
public static IList<Type> GetTypesByAssembly(string assemblyName)
{
var list = new List<Type>();
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyName(new AssemblyName(assemblyName));
var typeInfos = assembly.DefinedTypes;
foreach (var typeInfo in typeInfos)
{
list.Add(typeInfo.AsType());
}
return list;
}
public static Type GetImplementType(string typeName, Type baseInterfaceType)
{
return GetAllTypes().FirstOrDefault(t =>
{
if (t.Name == typeName &&
t.GetTypeInfo().GetInterfaces().Any(b => b.Name == baseInterfaceType.Name))
{
var typeInfo = t.GetTypeInfo();
return typeInfo.IsClass && !typeInfo.IsAbstract && !typeInfo.IsGenericType;
}
return false;
});
}
}
InternalApp:內部只用於初始化使用,獲取IServiceCollection、IServiceProvider、IConfiguration等幾個重要的內部物件
public static class InternalApp
{
internal static IServiceCollection InternalServices;
/// <summary>根服務</summary>
internal static IServiceProvider RootServices;
/// <summary>獲取Web主機環境</summary>
internal static IWebHostEnvironment WebHostEnvironment;
/// <summary>獲取泛型主機環境</summary>
internal static IHostEnvironment HostEnvironment;
/// <summary>配置物件</summary>
internal static IConfiguration Configuration;
public static void ConfigureApplication(this WebApplicationBuilder wab)
{
HostEnvironment = wab.Environment;
WebHostEnvironment = wab.Environment;
InternalServices = wab.Services;
}
public static void ConfigureApplication(this IConfiguration configuration)
{
Configuration = configuration;
}
public static void ConfigureApplication(this IHost app)
{
RootServices = app.Services;
}
}
App:實現非注入形式獲取Service、Options
public class App
{
static App()
{
EffectiveTypes = Assemblies.SelectMany(GetTypes);
}
private static bool _isRun;
/// <summary>是否正在執行</summary>
public static bool IsBuild { get; set; }
public static bool IsRun
{
get => _isRun;
set => _isRun = IsBuild = value;
}
/// <summary>應用有效程式集</summary>
public static readonly IEnumerable<Assembly> Assemblies = RuntimeExtension.GetAllAssemblies();
/// <summary>有效程式集型別</summary>
public static readonly IEnumerable<Type> EffectiveTypes;
/// <summary>優先使用App.GetService()手動獲取服務</summary>
public static IServiceProvider RootServices => IsRun || IsBuild ? InternalApp.RootServices : null;
/// <summary>獲取Web主機環境,如,是否是開發環境,生產環境等</summary>
public static IWebHostEnvironment WebHostEnvironment => InternalApp.WebHostEnvironment;
/// <summary>獲取泛型主機環境,如,是否是開發環境,生產環境等</summary>
public static IHostEnvironment HostEnvironment => InternalApp.HostEnvironment;
/// <summary>全域性配置選項</summary>
public static IConfiguration Configuration => InternalApp.Configuration;
/// <summary>
/// 獲取請求上下文
/// </summary>
public static HttpContext HttpContext => RootServices?.GetService<IHttpContextAccessor>()?.HttpContext;
//public static IUser User => GetService<IUser>();
#region Service
/// <summary>解析服務提供器</summary>
/// <param name="serviceType"></param>
/// <param name="mustBuild"></param>
/// <param name="throwException"></param>
/// <returns></returns>
public static IServiceProvider GetServiceProvider(Type serviceType, bool mustBuild = false, bool throwException = true)
{
if (HostEnvironment == null || RootServices != null &&
InternalApp.InternalServices
.Where(u =>
u.ServiceType ==
(serviceType.IsGenericType ? serviceType.GetGenericTypeDefinition() : serviceType))
.Any(u => u.Lifetime == ServiceLifetime.Singleton))
return RootServices;
//獲取請求生存週期的服務
if (HttpContext?.RequestServices != null)
return HttpContext.RequestServices;
if (RootServices != null)
{
IServiceScope scope = RootServices.CreateScope();
return scope.ServiceProvider;
}
if (mustBuild)
{
if (throwException)
{
throw new ApplicationException("當前不可用,必須要等到 WebApplication Build後");
}
return default;
}
ServiceProvider serviceProvider = InternalApp.InternalServices.BuildServiceProvider();
return serviceProvider;
}
public static TService GetService<TService>(bool mustBuild = true) where TService : class =>
GetService(typeof(TService), null, mustBuild) as TService;
/// <summary>獲取請求生存週期的服務</summary>
/// <typeparam name="TService"></typeparam>
/// <param name="serviceProvider"></param>
/// <param name="mustBuild"></param>
/// <returns></returns>
public static TService GetService<TService>(IServiceProvider serviceProvider, bool mustBuild = true)
where TService : class => (serviceProvider ?? GetServiceProvider(typeof(TService), mustBuild, false))?.GetService<TService>();
/// <summary>獲取請求生存週期的服務</summary>
/// <param name="type"></param>
/// <param name="serviceProvider"></param>
/// <param name="mustBuild"></param>
/// <returns></returns>
public static object GetService(Type type, IServiceProvider serviceProvider = null, bool mustBuild = true) =>
(serviceProvider ?? GetServiceProvider(type, mustBuild, false))?.GetService(type);
#endregion
#region private
/// <summary>載入程式集中的所有型別</summary>
/// <param name="ass"></param>
/// <returns></returns>
private static IEnumerable<Type> GetTypes(Assembly ass)
{
Type[] source = Array.Empty<Type>();
try
{
source = ass.GetTypes();
}
catch
{
Console.WriteLine($@"Error load `{ass.FullName}` assembly.");
}
return source.Where(u => u.IsPublic);
}
#endregion
#region Options
/// <summary>獲取配置</summary>
/// <typeparam name="TOptions">強型別選項類</typeparam>
/// <returns>TOptions</returns>
public static TOptions GetConfig<TOptions>()
where TOptions : class, IConfigurableOptions
{
TOptions instance = Configuration
.GetSection(ConfigurableOptions.GetConfigurationPath(typeof(TOptions)))
.Get<TOptions>();
return instance;
}
/// <summary>獲取選項</summary>
/// <typeparam name="TOptions">強型別選項類</typeparam>
/// <param name="serviceProvider"></param>
/// <returns>TOptions</returns>
public static TOptions GetOptions<TOptions>(IServiceProvider serviceProvider = null) where TOptions : class, new()
{
IOptions<TOptions> service = GetService<IOptions<TOptions>>(serviceProvider ?? RootServices, false);
return service?.Value;
}
/// <summary>獲取選項</summary>
/// <typeparam name="TOptions">強型別選項類</typeparam>
/// <param name="serviceProvider"></param>
/// <returns>TOptions</returns>
public static TOptions GetOptionsMonitor<TOptions>(IServiceProvider serviceProvider = null)
where TOptions : class, new()
{
IOptionsMonitor<TOptions> service =
GetService<IOptionsMonitor<TOptions>>(serviceProvider ?? RootServices, false);
return service?.CurrentValue;
}
/// <summary>獲取選項</summary>
/// <typeparam name="TOptions">強型別選項類</typeparam>
/// <param name="serviceProvider"></param>
/// <returns>TOptions</returns>
public static TOptions GetOptionsSnapshot<TOptions>(IServiceProvider serviceProvider = null)
where TOptions : class, new()
{
IOptionsSnapshot<TOptions> service = GetService<IOptionsSnapshot<TOptions>>(serviceProvider, false);
return service?.Value;
}
#endregion
}
IConfiguration物件從App中獲取
ApplicationSetup:透過事件獲取WebApplication的狀態。
public static class ApplicationSetup
{
public static void UseApplicationSetup(this WebApplication app)
{
app.Lifetime.ApplicationStarted.Register(() =>
{
App.IsRun = true;
});
app.Lifetime.ApplicationStopped.Register(() =>
{
App.IsRun = false;
//清除日誌
Log.CloseAndFlush();
});
}
}
11.3注入
var builder = WebApplication.CreateBuilder(args);
builder.Host
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureContainer<ContainerBuilder>(builder =>
{
builder.RegisterModule<AutofacModuleRegister>();
builder.RegisterModule<AutofacPropertityModuleReg>();
})
.ConfigureAppConfiguration((hostingContext, config) =>
{
hostingContext.Configuration.ConfigureApplication();
});
//配置
builder.ConfigureApplication();
app.ConfigureApplication();
app.UseApplicationSetup();
11.4使用
[HttpGet(Name = "GetUserInfo")]
public async Task<object> GetUserInfo()
{
var userServiceObjNew = App.GetService<IBaseServices<UserInfo, UserInfoVo>>(false);
var redisOptions = App.GetOptions<RedisOptions>();
await Console.Out.WriteLineAsync(JsonConvert.SerializeObject(redisOptions));
return await userServiceObjNew.Query();
}
12.ControllerAsServices屬性注入
IControllerActivator的預設實現不是ServiceBasedControllerActivator,而是DefaultControllerActivator。
控制器本身不是由依賴注入容器生成的,只不過是建構函式里的依賴是從容器裡拿出來的,控制器不是容器生成的,所以他的屬性也不是容器生成的。為了改變預設實現DefaultControllerActivator,所以使用ServiceBasedControllerActivator。
IControllerActivator原始碼地址:https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.Core/src/Controllers/IControllerActivator.cs
IControllerActivator 就倆個方法Create和Release。
檢視DefaultControllerActivator 和ServiceBasedControllerActivator原始碼發現:
DefaultControllerActivator是由ITypeActivatorCache.CreateInstance建立物件。
ServiceBasedControllerActivator是由actionContext.HttpContext.RequestServices建立物件。
透過改變Controllers的建立方式來實現屬性注入,將Controller的建立都由容器容器建立。以下倆種方式都是由容器建立Controller。
// 屬性注入
//builder.Services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());
// Add services to the container.
builder.Services.AddControllers().AddControllersAsServices();
Controller由容器建立完成,所以他的屬性也是容器建立的,就可以實現屬性注入。
屬性修飾詞必須是public
13.Redis分散式快取
13.1安裝Redis包
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.0" />
<PackageReference Include="StackExchange.Redis" Version="2.7.10" />
13.2Redis實現
這裡不貼程式碼了 框架裡都有
13.3IDistributedCache實現
IDistributedCache原始碼,IDistributedCache是微軟官方提供的快取介面標準。
實現ICaching,將IDistributedCache傳進去
13.4注入
如果開啟redis快取,實現RedisCacheImpl
如果開啟記憶體快取,實現MemoryDistributedCache
根據配置決定IDistributedCache的實現。
注入
13.5使用
[HttpGet(Name = "GetUserInfo")]
public async Task<object> GetUserInfo()
{
var cacheKey = "peng";
List<string> cacheKeys = await _caching.GetAllCacheKeysAsync();
await Console.Out.WriteLineAsync("全部keys -->" + JsonConvert.SerializeObject(cacheKeys));
await Console.Out.WriteLineAsync("新增一個快取");
await _caching.SetStringAsync(cacheKey, "pengpeng");
await Console.Out.WriteLineAsync("全部keys -->" + JsonConvert.SerializeObject(await _caching.GetAllCacheKeysAsync()));
await Console.Out.WriteLineAsync("當前key內容-->" + JsonConvert.SerializeObject(await _caching.GetStringAsync(cacheKey)));
await Console.Out.WriteLineAsync("刪除key");
await _caching.RemoveAsync(cacheKey);
await Console.Out.WriteLineAsync("全部keys -->" + JsonConvert.SerializeObject(await _caching.GetAllCacheKeysAsync()));
return "";
}
14.ORM
- 1、CURD + Page
- 2、多表聯查
- 3、欄位級別操作
- 4、國產資料庫
- 5、事務處理
- 6、多庫操作
- 7、多租戶/資料許可權
- 8、分庫分表操作
- 9、讀寫分離/從庫
- 10、災備資料庫
- 11、除錯SQL與日誌記錄
15.SqlSuger入門
15.1SqlSuger包安裝
<ItemGroup>
<PackageReference Include="SqlSugarCore" Version="5.1.4.145" />
</ItemGroup>
15.2SqlSuger實現
注入:
/// <summary>
/// SqlSugar 啟動服務
/// </summary>
public static class SqlsugarSetup
{
public static void AddSqlsugarSetup(this IServiceCollection services)
{
if (services == null) throw new ArgumentNullException(nameof(services));
// 預設新增主資料庫連線
if (!string.IsNullOrEmpty(AppSettings.app("MainDB")))
{
MainDb.CurrentDbConnId = AppSettings.app("MainDB");
}
BaseDBConfig.MutiConnectionString.allDbs.ForEach(m =>
{
var config = new ConnectionConfig()
{
ConfigId = m.ConnId.ObjToString().ToLower(),
ConnectionString = m.Connection,
DbType = (DbType)m.DbType,
IsAutoCloseConnection = true,
MoreSettings = new ConnMoreSettings()
{
IsAutoRemoveDataCache = true,
SqlServerCodeFirstNvarchar = true,
},
InitKeyType = InitKeyType.Attribute
};
if (SqlSugarConst.LogConfigId.ToLower().Equals(m.ConnId.ToLower()))
{
BaseDBConfig.LogConfig = config;
}
else
{
BaseDBConfig.ValidConfig.Add(config);
}
BaseDBConfig.AllConfigs.Add(config);
});
if (BaseDBConfig.LogConfig is null)
{
throw new ApplicationException("未配置Log庫連線");
}
// SqlSugarScope是執行緒安全,可使用單例注入
// 參考:https://www.donet5.com/Home/Doc?typeId=1181
services.AddSingleton<ISqlSugarClient>(o =>
{
return new SqlSugarScope(BaseDBConfig.AllConfigs);
});
}
}
// Sqlsugar ORM
builder.Services.AddSqlsugarSetup();
15.3BaseRepository
ISqlSugarClient只讀,外部只可獲取,不可修改
15.4BaseServices
15.5使用
屬性注入
16.SqlSuger事務簡單用法
16.1實現IUnitOfWorkManage
實現IUnitOfWorkManage,透過依賴注入ISqlSugarClient獲取例項實現事務
16.2BeginTran
標準的開啟、提交、回滾事物的寫法
16.3UnitOfWork
透過解構函式實現事務,此時不需要回滾事務。
當Dispose時,會自動回滾事務
17.SqlSuger事務高階用法
動態代理實現事務
事務傳播方式
如果有一個[UseTran(Propagation = Propagation.Required)]就開啟事務
ConcurrentStack
第一個方法進來後開啟事務,將方法加入佇列。多個方法加入,只會在第一次開啟事務。
執行完一個事務後在執行下一個事務。
當所有方法執行完成後,執行After。
獲取棧中第一個方法。
然後提交事務。
如果異常就回滾事務。
最後再從棧中第一個方法開始全部移除,直到刪除完成。
如果異常就直接回滾。
移除所有方法並回滾。
18.SqlSuger多庫操作
Tenant指定資料庫配置,不區分大小寫。
SugarTable指定資料庫表明。
ISqlSugarClient根據類上配置的資料庫和表名獲取資料庫例項,從而實現多庫。
建議使用GetConnectionScope,是執行緒安全的。
對內_db,對外_Db,_dbBase預設主庫
19.SqlSuger分庫分表
設定分表策略,這裡是按月分表。
SplitField,設定分表欄位,根據建立時間來分表。
倉儲和服務實現分表新增和查詢。
分表查詢和新增。
20.授權認證[Authorize]入門
1、理解[Authorize]特性
2、JWT組成和安全設計
3、Claims宣告和安全設計
4、HttpContext上下文的處理
5、基於Role、Claims的授權
6、基於Requirement的複雜授權
7、分散式微服務下的統一授權
8、認證中心的設計、單點登入
9、微前端 + 微服務的入口網站設計
10、其他應用技巧(資料許可權、租戶等)
在Controller加上特性[Authorize]
訪問介面時會報錯,因為你選擇加上認證特性,需要指定認證方案。
包安裝:
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.2" />
</ItemGroup>
在Program加上認證方式。
// JWT
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true, //是否驗證Issuer
ValidateAudience = true, //是否驗證Audience
ValidateLifetime = true, //是否驗證失效時間
ValidateIssuerSigningKey = true, //是否驗證SecurityKey
ValidIssuer = "Peng.Core", //發行人 //Issuer,這兩項和前面簽發jwt的設定一致
ValidAudience = "wr", //訂閱人
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("sdfsdfsrty45634kkhllghtdgdfss345t678fs"))//拿到SecurityKey
};
});
21.基於原始碼分析Claims宣告
21.1原始碼分析
app.UseAuthorization():開啟授權
AuthorizationMiddlewareInternal:使用授權中介軟體
AuthorizationMiddleware:在授權中介軟體中新增策略
AuthorizationPolicyBuilder:透過角色授權
RolesAuthorizationRequirement:在請求上下文中獲取角色資訊
21.2許可權控制
只允許SuperAdmin角色訪問
21.3基於Claim和Role授權
21.4Claims宣告產生
注入請求上下文服務
從請求上下文獲取Claims
22.基於原始碼分析策略授權
22.1原始碼分析
AuthorizationMiddleware:授權中介軟體。
將所有策略新增到IList
AuthorizationPolicyBuilder:所以自定策略需要實現IAuthorizationRequirement介面。
IPolicyEvaluator:處理授權。
IPolicyEvaluator:AuthenticateAsync處理策略授權。
AuthorizationPolicyBuilder:Claim宣告授權。
AuthorizationPolicyBuilder:角色授權。
Claim宣告授權和角色授權都實現了AuthorizationHandler、IAuthorizationRequirement
22.2自定義策略授權
看上邊的原始碼分析實現Claim宣告授權和角色授權都實現了AuthorizationHandler、IAuthorizationRequirement。
所以自定策略授權也需要實現AuthorizationHandler、IAuthorizationRequirement。
在Program注入。
Controller加上特性自定義策略。
23.複雜策略授權
實現: AuthorizationHandler
獲取角色所有選單許可權。
或者當介面沒有許可權資訊時去讀取資料庫進行初始化
判斷登入狀態,如果登入判斷token是否失效,失效就重新登入。
判斷角色許可權
獲取token
24.微服務鑑權
- 分散式微服務下的統一鑑權
- 第一種服務例項少,內部鑑權,基於角色。
- 第二種服務例項多,需要管理介面,需要新增授權服務。
- 認證中心、單點登入:
- 和其他服務同級域名(一級域名)
- 攜帶SSO,在目標域名解析SSO,實現單點登入
- 微服務、微前端:
- 其他應用(資料許可權、租戶等)
25.資料許可權-欄位多租戶
25.1HttpContextAccessor獲取使用者資訊
定義IUser介面並且實現
AspNetUser從HttpContext請求獲取使用者資訊
IUser注入
25.2SqlSuger實現欄位多租戶
全域性配置User資訊
將租戶欄位配置為查詢過濾條件
TenantId == 0,代表公共資料,所有人可見。
SqlSugar配置資料許可權
增加一個資料許可權表,所有租戶的表都要繼承ITenantEntity
配置實體對映
多租戶測試
26.資料許可權-分表多租戶
資料許可權比選單許可權更細緻。
分表多租戶的表需要加上MultiTenant特性
租戶隔離方案
獲取Peng.Net8.Model名稱空間下所有實體
篩選出來有MultiTenant特性並且是表隔離的。
db.MappingTables.Add:將資料庫表明中TableName換成了TableName_TenantId。
分表多租戶測試
27.資料許可權-分庫多租戶
約定大於配置。
系統租戶表。
DbType是資料庫型別,這裡是SqlLite。
Connection是資料庫連結配置。
分表多租戶資料表的資料庫格式是TableName_Id(表名_租戶ID)。
增加系統租戶表,增加庫隔離的隔離方案列舉。
讀取系統租戶表的租戶配置,將租戶配置的資料庫連結新增到SqlSugar。
新增多租戶的業務表實體和實體檢視,並配置實體對映。
分庫多租戶測試
注意需要配置的地方:
- SysUserInfo表的登入使用者的TenantId,將這個Id配置到SysTenant表的Id
- SysTenant資料庫連結,因為是SqlLite本地資料庫,注意地址。
測試
28.SqlSugar日誌和快取
28.1SqlSugar日誌
實現SqlSugarAop,面向切面思想。
配置到SqlSugar。
使用登入介面,日誌已經列印出來了。
28.2SqlSugar快取
實現SqlSugarCacheService,繼承SqlSugar的ICacheService。
開啟快取。
BaseRepository實現QueryWithCache快取查詢。
最佳化系統租戶表查詢,如果表沒有更新會查詢快取。
BaseServices實現QueryWithCache快取查詢。
測試快取查詢
需要注意的是如果手動更改資料庫資料,快取不會更新。
快取只有程式碼增刪改的時候才會更新快取。