AspNetCore8.0實戰

peng_boke發表於2024-03-15

前言

想變優秀的第N天。
學習張老師的Blog.Core。

1.建立Asp.NetCore API

1.1建立專案

啟用OpenAPI:sawgger

不適用頂級語句:使用main函式

使用控制器:controller

image-20240304230411871

1.2配置說明

iisSettings:iis配置。

http:kestrl啟動配置。

IIS Express:iis啟動配置。

image-20240306215443679

2.倉儲+服務

建立以下公共類庫和API,他們分別是:

Peng.Net8:WebApi。

Peng.Net8.Common:公共幫助類。

Peng.Net8.Model:實體層。

Peng.Net8.Repository:倉儲層。

Peng.Net8.IService:服務介面層。

Peng.Net8.Service:服務層。

image-20240306221548968

3.泛型基類

3.1倉儲基類

IBaseRepository:需要對傳入TEntity(實體模型)進行資料操作。

image-20240306222307554

BaseRepository:實現IBaseRepository。

image-20240306222446728

3.2服務基類

IBaseServices:對傳入的TEntity(實體模型)進行操作,但是不能返回TEntity,需要返回TVo(檢視模型),不能將實體欄位暴露給WebAPI層。

image-20240306222600955

BaseServices:實現IBaseServices。

image-20240306222754601

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" />

image-20240306223742861

4.2使用

將UserInfo和UserInfoVo的欄位進行配置。

image-20240306225451692

AutoMapperConfig:

/// <summary>
///  靜態全域性 AutoMapper 配置檔案
/// </summary>
public class AutoMapperConfig
{
    public static MapperConfiguration RegisterMappings()
    {
        return new MapperConfiguration(cfg =>
        {
            cfg.AddProfile(new CustomProfile());
        });
    }
}

image-20240306225253523

注入:

 builder.Services.AddAutoMapper(typeof(AutoMapperConfig));
 AutoMapperConfig.RegisterMappings();

image-20240306225204196

5.原生依賴注入

在NetCore開發中,減少new的使用,儘量使用依賴注入。(不應該使用new,比如複雜的鏈路、GC回收、記憶體洩露等)

AddSington:單例模式。

AddScope:會話模式。一個http請求。

AddTrasient:瞬時模式。

5.1倉儲服務程式碼

倉儲層:

image-20240306234227162

image-20240306234309779

服務層:

image-20240306234339781

image-20240306234359121

5.2依賴注入

注入:

image-20240306234502236

image-20240306234600341

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 // 可選,新增目錄
}

image-20240307222935800

6.2安裝模板

安裝模板 (絕對路徑)

dotnet new install 絕對路徑\Peng.Net8  --force

如果需要解除安裝重來的話,解除安裝模版命令

dotnet new uninstall 絕對路徑\Peng.Net8

image-20240307212642337

檢視命令

donet new list

image-20240307213039566

檢視模版支援選項,使用的名稱是template.json中的shortName

dotnet new 名稱 -h

image-20240307215351802

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 自定義命令 (生成專案模式)

image-20240307215901134

建立成功

image-20240307215925305

image-20240307223035249

VS2022建立

image-20240307223620819

6.4nuget

下載nuget.exe檔案

https://www.nuget.org/downloads

image-20240307214916236

打包模板,並生成.nupkg檔案

nuget.exe pack Peng.Net8/peng.net8.template.nuspec

image-20240309193010099

image-20240307221021494

釋出到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>

image-20240307230710065

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建構函式注入

在建構函式中賦值:

image-20240307232716650

7.4屬性注入

ASP.NET Core預設不使用DI獲取Controller,是因為DI容器構建完成後就不能變更了,但是Controller是可能有動態載入的需求的。

需要使用IControllerActivator開啟Controller的屬性注入,預設不開啟。

image-20240307232750159

必須使用public修飾屬性

image-20240307232855819

8.AOP(Log)

8.1安裝

<PackageReference Include="Autofac.Extras.DynamicProxy" Version="7.1.0" />	

image-20240309192036214

8.2動態代理實現ServiceAOP

實現IInterceptor,執行完成後打出日誌

image-20240309192322499

   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)注入日誌。

image-20240309192532970

  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實現

日誌成功打出。

image-20240315145515808

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));

image-20240308230257272

9.4使用

var redisEnable = AppSettings.app(new string[] { "Redis", "Enable" });
var redisConnectionString = AppSettings.GetValue("Redis:ConnectionString");
Console.WriteLine($"Enable: {redisEnable} ,  ConnectionString: {redisConnectionString}");

image-20240315145908176

配置檔案:

image-20240308230555213

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();  

image-20240308232544721

10.5使用

var redisOptions = _redisOptions.Value;

image-20240315154145877

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中獲取

image-20240309181519681

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();
        });
    }
}

image-20240309181931782

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();

image-20240309182003197

app.ConfigureApplication();
app.UseApplicationSetup();

image-20240309182040002

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();
 }

image-20240315154111822

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。

image-20240309184511885

檢視DefaultControllerActivator 和ServiceBasedControllerActivator原始碼發現:

DefaultControllerActivator是由ITypeActivatorCache.CreateInstance建立物件。

ServiceBasedControllerActivator是由actionContext.HttpContext.RequestServices建立物件。

image-20240309184911906

透過改變Controllers的建立方式來實現屬性注入,將Controller的建立都由容器容器建立。以下倆種方式都是由容器建立Controller。

image-20240309185138521

// 屬性注入
//builder.Services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());
// Add services to the container.
builder.Services.AddControllers().AddControllersAsServices();

Controller由容器建立完成,所以他的屬性也是容器建立的,就可以實現屬性注入。

屬性修飾詞必須是public

image-20240309185300054

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實現

這裡不貼程式碼了 框架裡都有

image-20240309195841073

13.3IDistributedCache實現

IDistributedCache原始碼,IDistributedCache是微軟官方提供的快取介面標準。

image-20240309200548124

實現ICaching,將IDistributedCache傳進去

image-20240309201729222

13.4注入

如果開啟redis快取,實現RedisCacheImpl

image-20240309204212900

如果開啟記憶體快取,實現MemoryDistributedCache

image-20240309204327607

image-20240309204116478

根據配置決定IDistributedCache的實現。

image-20240309204413241

注入

image-20240309204605635

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 "";
 }

image-20240309204938350

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>

image-20240309230319901

15.2SqlSuger實現

image-20240309230633371

注入:

/// <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();

image-20240309224007390

15.3BaseRepository

ISqlSugarClient只讀,外部只可獲取,不可修改

image-20240309230419868

15.4BaseServices

image-20240309230547998

15.5使用

屬性注入

image-20240309230802198

16.SqlSuger事務簡單用法

16.1實現IUnitOfWorkManage

實現IUnitOfWorkManage,透過依賴注入ISqlSugarClient獲取例項實現事務

image-20240309232859389

16.2BeginTran

標準的開啟、提交、回滾事物的寫法

image-20240309233543435

16.3UnitOfWork

透過解構函式實現事務,此時不需要回滾事務。

image-20240309234039475

當Dispose時,會自動回滾事務

image-20240309234213036

17.SqlSuger事務高階用法

動態代理實現事務

image-20240310005051193

事務傳播方式

image-20240310004807253

如果有一個[UseTran(Propagation = Propagation.Required)]就開啟事務

image-20240310005148575

ConcurrentStack是執行緒安全的後進先出 (LIFO:棧) 集合。

第一個方法進來後開啟事務,將方法加入佇列。多個方法加入,只會在第一次開啟事務。

image-20240310005445803

執行完一個事務後在執行下一個事務。

image-20240310004927010

當所有方法執行完成後,執行After。

image-20240310010004953

獲取棧中第一個方法。

然後提交事務。

如果異常就回滾事務。

最後再從棧中第一個方法開始全部移除,直到刪除完成。

image-20240310010355059

如果異常就直接回滾。

image-20240310010926504

移除所有方法並回滾。

image-20240310010958893

18.SqlSuger多庫操作

Tenant指定資料庫配置,不區分大小寫。

SugarTable指定資料庫表明。

image-20240310014714012

image-20240310013915191

ISqlSugarClient根據類上配置的資料庫和表名獲取資料庫例項,從而實現多庫。

建議使用GetConnectionScope,是執行緒安全的。

image-20240310014036916

對內_db,對外_Db,_dbBase預設主庫

image-20240310014323086

19.SqlSuger分庫分表

設定分表策略,這裡是按月分表。image-20240310194832380

SplitField,設定分表欄位,根據建立時間來分表。

image-20240310194920565

倉儲和服務實現分表新增和查詢。

image-20240310195126520

分表查詢和新增。

image-20240310195217811

20.授權認證[Authorize]入門

1、理解[Authorize]特性

2、JWT組成和安全設計

3、Claims宣告和安全設計

4、HttpContext上下文的處理

5、基於Role、Claims的授權

6、基於Requirement的複雜授權

7、分散式微服務下的統一授權

8、認證中心的設計、單點登入

9、微前端 + 微服務的入口網站設計

10、其他應用技巧(資料許可權、租戶等)

image-20240310200148254

在Controller加上特性[Authorize]

image-20240310203010367

訪問介面時會報錯,因為你選擇加上認證特性,需要指定認證方案。

image-20240310203243639
包安裝:

  <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
        };
    });

image-20240310203426208

21.基於原始碼分析Claims宣告

21.1原始碼分析

app.UseAuthorization():開啟授權

image-20240310212410094

AuthorizationMiddlewareInternal:使用授權中介軟體

image-20240310212457693

AuthorizationMiddleware:在授權中介軟體中新增策略

image-20240310212629549

AuthorizationPolicyBuilder:透過角色授權

image-20240310212742393

RolesAuthorizationRequirement:在請求上下文中獲取角色資訊

image-20240310212821200

21.2許可權控制

只允許SuperAdmin角色訪問

image-20240310213140196

21.3基於Claim和Role授權

image-20240310213616529

image-20240310213636110

21.4Claims宣告產生

注入請求上下文服務

image-20240310213857396

從請求上下文獲取Claims

image-20240310213927856

22.基於原始碼分析策略授權

22.1原始碼分析

AuthorizationMiddleware:授權中介軟體。

將所有策略新增到IList集合中一起處理。

image-20240310220529792

AuthorizationPolicyBuilder:所以自定策略需要實現IAuthorizationRequirement介面。

image-20240310220635240

IPolicyEvaluator:處理授權。

image-20240310221109010

IPolicyEvaluator:AuthenticateAsync處理策略授權。

image-20240310221654841

AuthorizationPolicyBuilder:Claim宣告授權。

image-20240310221230605

AuthorizationPolicyBuilder:角色授權。

image-20240310221311562

Claim宣告授權和角色授權都實現了AuthorizationHandler、IAuthorizationRequirement

image-20240310221416291

22.2自定義策略授權

看上邊的原始碼分析實現Claim宣告授權和角色授權都實現了AuthorizationHandler、IAuthorizationRequirement。

所以自定策略授權也需要實現AuthorizationHandler、IAuthorizationRequirement。

image-20240310221932618

在Program注入。

image-20240310222030736

Controller加上特性自定義策略。

image-20240310222139663

23.複雜策略授權

實現: AuthorizationHandler, IAuthorizationRequirement

image-20240310232636900

獲取角色所有選單許可權。

或者當介面沒有許可權資訊時去讀取資料庫進行初始化

image-20240310232719979

判斷登入狀態,如果登入判斷token是否失效,失效就重新登入。

image-20240310233113085

判斷角色許可權

image-20240310233409051

獲取token

image-20240310234917309

24.微服務鑑權

  • 分散式微服務下的統一鑑權
    • 第一種服務例項少,內部鑑權,基於角色。
    • 第二種服務例項多,需要管理介面,需要新增授權服務。
  • 認證中心、單點登入:
    • 和其他服務同級域名(一級域名)
    • 攜帶SSO,在目標域名解析SSO,實現單點登入
  • 微服務、微前端:
  • 其他應用(資料許可權、租戶等)

25.資料許可權-欄位多租戶

25.1HttpContextAccessor獲取使用者資訊

定義IUser介面並且實現

image-20240313223052746

AspNetUser從HttpContext請求獲取使用者資訊

image-20240313223117421

IUser注入

image-20240313223217813

25.2SqlSuger實現欄位多租戶

全域性配置User資訊

image-20240313223958983

將租戶欄位配置為查詢過濾條件

TenantId == 0,代表公共資料,所有人可見。

image-20240313224051017

SqlSugar配置資料許可權

image-20240313224244568

增加一個資料許可權表,所有租戶的表都要繼承ITenantEntity

image-20240313224701603

配置實體對映

image-20240313224750243

多租戶測試

image-20240313224840308

26.資料許可權-分表多租戶

資料許可權比選單許可權更細緻。

分表多租戶的表需要加上MultiTenant特性

image-20240313231850071

租戶隔離方案

image-20240313231939252

獲取Peng.Net8.Model名稱空間下所有實體

image-20240313232654753

篩選出來有MultiTenant特性並且是表隔離的。

db.MappingTables.Add:將資料庫表明中TableName換成了TableName_TenantId。image-20240313232125672

分表多租戶測試

image-20240313233337552

27.資料許可權-分庫多租戶

約定大於配置。

系統租戶表。

DbType是資料庫型別,這裡是SqlLite。

Connection是資料庫連結配置。

分表多租戶資料表的資料庫格式是TableName_Id(表名_租戶ID)。

image-20240314212958556

增加系統租戶表,增加庫隔離的隔離方案列舉。

image-20240314214212543

讀取系統租戶表的租戶配置,將租戶配置的資料庫連結新增到SqlSugar。

image-20240314214927452

新增多租戶的業務表實體和實體檢視,並配置實體對映。

image-20240314215446734

分庫多租戶測試

image-20240314215710844

注意需要配置的地方:

  • SysUserInfo表的登入使用者的TenantId,將這個Id配置到SysTenant表的Id
  • SysTenant資料庫連結,因為是SqlLite本地資料庫,注意地址。

image-20240314223023274

image-20240314223221777

測試

image-20240314222940323

28.SqlSugar日誌和快取

28.1SqlSugar日誌

實現SqlSugarAop,面向切面思想。

image-20240314224221945

配置到SqlSugar。

image-20240314224323970

使用登入介面,日誌已經列印出來了。

image-20240315164234557

28.2SqlSugar快取

實現SqlSugarCacheService,繼承SqlSugar的ICacheService。

image-20240314225348347

開啟快取。

image-20240314225511986

BaseRepository實現QueryWithCache快取查詢。

image-20240314225633045

最佳化系統租戶表查詢,如果表沒有更新會查詢快取。

image-20240314225714937

BaseServices實現QueryWithCache快取查詢。

image-20240314225811999

測試快取查詢

image-20240314225919802

需要注意的是如果手動更改資料庫資料,快取不會更新。

快取只有程式碼增刪改的時候才會更新快取。

image-20240314230025687