Fireasy3 揭秘 -- 使用 SourceGeneraor 實現動態代理(AOP)

来源:https://www.cnblogs.com/fireasy/archive/2023/03/05/17179651.html
-Advertisement-
Play Games

目錄 Fireasy3 揭秘 -- 依賴註入與服務發現 Fireasy3 揭秘 -- 自動服務部署 Fireasy3 揭秘 -- 使用 SourceGeneraor 改進服務發現 Fireasy3 揭秘 -- 使用 SourceGeneraor 實現動態代理(AOP) Fireasy3 揭秘 -- ...


目錄

  • Fireasy3 揭秘 -- 依賴註入與服務發現
  • Fireasy3 揭秘 -- 自動服務部署
  • Fireasy3 揭秘 -- 使用 SourceGeneraor 改進服務發現
  • Fireasy3 揭秘 -- 使用 SourceGeneraor 實現動態代理(AOP)
  • Fireasy3 揭秘 -- 使用 Emit 構建程式集
  • Fireasy3 揭秘 -- 使用緩存提高反射性能
  • Fireasy3 揭秘 -- 動態類型及擴展支持
  • Fireasy3 揭秘 -- 線程數據共用的實現
  • Fireasy3 揭秘 -- 配置管理及解析處理
  • Fireasy3 揭秘 -- 資料庫適配器
  • Fireasy3 揭秘 -- 解決資料庫之間的語法差異
  • Fireasy3 揭秘 -- 獲取資料庫的架構信息
  • Fireasy3 揭秘 -- 數據批量插入的實現
  • Fireasy3 揭秘 -- 使用包裝器對數據讀取進行相容
  • Fireasy3 揭秘 -- 數據行映射器
  • Fireasy3 揭秘 -- 數據轉換器的實現
  • Fireasy3 揭秘 -- 通用序列生成器和雪花生成器的實現
  • Fireasy3 揭秘 -- 命令攔截器的實現
  • Fireasy3 揭秘 -- 資料庫主從同步的實現
  • Fireasy3 揭秘 -- 大數據分頁的策略
  • Fireasy3 揭秘 -- 數據按需更新及生成實體代理類
  • Fireasy3 揭秘 -- 用對象池技術管理上下文
  • Fireasy3 揭秘 -- Lambda 表達式解析的原理
  • Fireasy3 揭秘 -- 擴展選擇的實現
  • Fireasy3 揭秘 -- 按需載入與惰性載入的區別與實現
  • Fireasy3 揭秘 -- 自定義函數的解析與綁定
  • Fireasy3 揭秘 -- 與 MongoDB 進行適配
  • Fireasy3 揭秘 -- 模塊化的實現原理

  實現 AOP(面向切麵編程)的實現方式有很多種,但無外乎靜態紡織和動態編織兩種。

  動態編織 在 Fireasy 2 中使用 Emit 實現了動態編織。Emit 的缺點很明顯,首先就是要求對 IL 語言必須比較熟悉且考慮較全面,其次由於是動態編織,需要使用記憶體緩存來對代理類進行管理。
  靜態編織 是指在代碼編譯時就將攔截器相關的代碼插入到代碼內部,運行時不耗用時間和記憶體空間。常用的有 MSBuild(如PostSharp) 和 Code Analyzers(本文中用到的 ISourceGenerator)。

  首先,定義攔截器的介面,Initialize/InitializeAsync 方法用於首次初始化,Intercept/InterceptAsync 用於攔截方法的執行和屬性的訪問。如下:

    /// <summary>
    /// 提供對類成員進行攔截的方法。
    /// </summary>
    public interface IInterceptor
    {
        /// <summary>
        /// 使用上下文對象對當前的攔截器進行初始化。
        /// </summary>
        /// <param name="context">包含攔截定義的上下文。</param>
        void Initialize(InterceptContext context);

        /// <summary>
        /// 將自定義方法註入到當前的攔截點。
        /// </summary>
        /// <param name="info">攔截調用信息。</param>
        void Intercept(InterceptCallInfo info);
    }

    /// <summary>
    /// 提供對類成員進行攔截的非同步方法。
    /// </summary>
    public interface IAsyncInterceptor
    {
        /// <summary>
        /// 使用上下文對象對當前的攔截器進行初始化。
        /// </summary>
        /// <param name="context">包含攔截定義的上下文。</param>
        ValueTask InitializeAsync(InterceptContext context);

        /// <summary>
        /// 將自定義方法註入到當前的攔截點。
        /// </summary>
        /// <param name="info">攔截調用信息。</param>
        ValueTask InterceptAsync(InterceptCallInfo info);
    }

  InterceptCallInfo 是攔截的調用信息,其中,Arguments 屬性是調用的入參,ReturnValue 屬性是返回的值。如下:

    /// <summary>
    /// 用於通知客戶端的攔截信息。無法繼承此類。
    /// </summary>
    public sealed class InterceptCallInfo
    {
        /// <summary>
        /// 獲取或設置定義的類型。
        /// </summary>
        public Type? DefinedType { get; set; }

        /// <summary>
        /// 獲取或設置當前被攔截的方法或屬性。
        /// </summary>
        public MemberInfo? Member { get; set; }

        /// <summary>
        /// 獲取或設置方法的返回類型。
        /// </summary>
        public Type? ReturnType { get; set; }

        /// <summary>
        /// 獲取或設置當前被攔截的目標對象。
        /// </summary>
        public object? Target { get; set; }

        /// <summary>
        /// 獲取或設置攔截的類型。
        /// </summary>
        public InterceptType InterceptType { get; set; }

        /// <summary>
        /// 獲取或設置方法的參數數組。
        /// </summary>
        public object[]? Arguments { get; set; }

        /// <summary>
        /// 獲取或設置方法的返回值。
        /// </summary>
        public object? ReturnValue { get; set; }

        /// <summary>
        /// 獲取或設置觸發的異常信息。
        /// </summary>
        public Exception? Exception { get; set; }

        /// <summary>
        /// 獲取或設置取消 Before 事件之後調用基類的方法。
        /// </summary>
        public bool Cancel { get; set; }

        /// <summary>
        /// 獲取或設置是否中斷後繼攔截器的執行。
        /// </summary>
        public bool Break { get; set; }
    }

  最後定義一個特性 InterceptAttribute 用於指定類或方法上的攔截器類型,只需要在類、屬性或方法上指定即可:

[InterceptAttribute(typeof(SampleInterceptor))]

  接下來,我們定義好註入代碼的原型,以便下一步生成代碼。比如方法的切麵示例:

public override string Test(string str)
{
    //定義由 InterceptAttribute 指定的攔截器實例
    var interceptors = new List<IInterceptor> { new SampleInterceptor() };
    var info = new InterceptCallInfo();
    //攔截的對象為當前對象
    info.Target = this;
    //方法的入參
    info.Arguments = new object[] { str };
    //當前攔截的成員(方法)
    info.Member = ((MethodInfo)MethodBase.GetCurrentMethod()).GetBaseDefinition();

    try
    {
        //初始化
        _Initialize(interceptors, info);
        //通知攔截器方法即將被調用
        _Intercept(interceptors, info, InterceptType.BeforeMethodCall);

        //如果攔截器告知要取消執行
        if (info.Cancel)
        {
            //返回值,如果方法為 void,那麼直接 return
            //在攔截器中可以給 ReturnValue 進行賦值,然後 Cancel = true
            return info.ReturnValue == null ? default : (string)info.ReturnValue;
        }
        //調用父方法
        info.ReturnValue = base.Test(str);
        //通知攔截器方法已調用完成
        _Intercept(interceptors, info, InterceptType.AfterMethodCall);
    }
    catch (System.Exception exp)
    {
        info.Exception = exp;
        //通知攔截器,有異常拋出
        _Intercept(interceptors, info, InterceptType.Catching);
        throw exp;
    }
    finally
    {
        //任何時候,都會走到這一步
        _Intercept(interceptors, info, InterceptType.Finally);
    }
    //返回值
    return info.ReturnValue == null ? default : (string)info.ReturnValue;
}

  屬性的切麵也差不多,只是對 get 和 set 分別處理。我們來看一下 _Initialize 方法要實現的目的:

private void _Initialize(List<IInterceptor> interceptors, InterceptCallInfo callInfo)
{
    if (!this._initMarks.Contains(callInfo.Member))
    {
        for (int i = 0; i < interceptors.Count; i++)
        {
            InterceptContext context = new InterceptContext(callInfo.Member, this);
            interceptors[i].Initialize(context);
        }
        this._initMarks.Add(callInfo.Member);
    }
}

  它的目的是讓攔截器只調用一次 Initialize 方法。而 Intercept 方法用於通知攔截器進行攔截,在攔截器里給定 Break = true 時,後續的攔截器將不會被調用。

private void _Intercept(List<IInterceptor> interceptors, InterceptCallInfo callInfo, InterceptType interceptType)
{
    callInfo.InterceptType = interceptType;
    callInfo.Break = false;
    for (int i = 0; i < interceptors.Count; i++)
    {
        if (callInfo.Break)
        {
            break;
        }

        interceptors[i].Intercept(callInfo);
    }
}

  接下來,我們用 ISourceGenretor 來實現代碼的生成。

  上篇 使用 SourceGeneraor 改進服務發現 已經講解過 ISourceGenerator 的用法了,需要分別定義一個 ISyntaxContextReceiverISourceGenerator 的實現。
  定義 DynamicProxySyntaxReceiver 類,用來接收語法節點,併進行分析,找出可以攔截的方法和屬性,以及攔截器等內容。這裡,我著重講解一下 AnalyseClassSyntax 方法的實現,如下:

        /// <summary>
        /// 分析類型語法。
        /// </summary>
        /// <param name="model"></param>
        /// <param name="syntax"></param>
        private void AnalyseClassSyntax(SemanticModel model, ClassDeclarationSyntax syntax)
        {
            var typeSymbol = (ITypeSymbol)model.GetDeclaredSymbol(syntax)!;
            if (typeSymbol.IsSealed)
            {
                return;
            }

            var interceptorMetadataOfClass = FindInterceptorMetadata(typeSymbol);

            var metadata = new ClassMetadata(typeSymbol);

            //獲取所有成員
            foreach (var memberSymbol in typeSymbol.GetMembers())
            {
                //如果不是方法或屬性
                if (memberSymbol.Kind != SymbolKind.Method && memberSymbol.Kind != SymbolKind.Property)
                {
                    continue;
                }

                if (memberSymbol is IMethodSymbol method)
                {
                    //構造器需要重載,所以也要記錄下來
                    if (method.MethodKind == MethodKind.Constructor)
                    {
                        metadata.AddConstructor(method);
                        continue;
                    }

                    if (method.MethodKind != MethodKind.Ordinary)
                    {
                        continue;
                    }
                }

                //方法定義為 virtual 並且是公共的
                if (!memberSymbol.IsVirtual || memberSymbol.DeclaredAccessibility != Accessibility.Public)
                {
                    continue;
                }

                //查找方法上的 InterceptAttribute 特性
                if (memberSymbol.GetAttributes().Any(s => s.AttributeClass!.ToDisplayString() == InterceptorAttributeName))
                {
                    var interceptorMetadataOfMember = FindInterceptorMetadata(memberSymbol);
                    if (interceptorMetadataOfMember != null)
                    {
                        metadata.AddMember(memberSymbol, interceptorMetadataOfMember);
                    }
                }
                //沒找到方法上的特性,則使用類上定義的 InterceptAttribute 特性
                else if (interceptorMetadataOfClass != null)
                {
                    var hasIgnoreThrowExpAttr = HasIgnoreThrowExceptionAttribute(memberSymbol);
                    metadata.AddMember(memberSymbol, interceptorMetadataOfClass.Clone(!hasIgnoreThrowExpAttr));
                }
            }

            if (metadata.IsValid)
            {
                _metadata.Add(FindUsings(syntax, metadata));
            }
        }

  FindInterceptorMetadata 方法用於查找在類或方法、屬性上定義的 InterceptAttribute 特性,並記錄下攔截器的類型。如下:

        /// <summary>
        /// 獲取攔截器的元數據。
        /// </summary>
        /// <param name="symbol"></param>
        /// <returns></returns>
        private InterceptorMetadata? FindInterceptorMetadata(ISymbol symbol)
        {
            var types = new List<ITypeSymbol>();
            foreach (AttributeData classAttr in symbol.GetAttributes().Where(s => s.AttributeClass!.ToDisplayString() == InterceptorAttributeName))
            {
                var interceptorType = GetInterceptorType(classAttr.ConstructorArguments[0].Value);
                if (interceptorType != null)
                {
                    types.Add(interceptorType);
                }
            }

            if (!types.Any())
            {
                return null;
            }

            var hasIgnoreThrowExpAttr = HasIgnoreThrowExceptionAttribute(symbol);

            return new InterceptorMetadata(types, !hasIgnoreThrowExpAttr);
        }

  在 ClassMetadata 里,定義了可被攔截的方法、屬性,以及構造器、引用的命名空間等信息,如下:

    /// <summary>
    /// 類的元數據。
    /// </summary>
    public class ClassMetadata
    {
        /// <summary>
        /// 初始化 <see cref="ClassMetadata"/> 類的新實例。
        /// </summary>
        /// <param name="type">類型符號。</param>
        public ClassMetadata(ITypeSymbol type)
        {
            Type = type;
        }

        /// <summary>
        /// 獲取類型符號。
        /// </summary>
        public ITypeSymbol Type { get; }

        /// <summary>
        /// 獲取命名空間。
        /// </summary>
        public string Namespace => Type.ContainingNamespace.ToDisplayString();

        /// <summary>
        /// 獲取類型的全名。
        /// </summary>
        public string TypeFullName => Type.ToDisplayString();

        /// <summary>
        /// 獲取代理類的名稱。
        /// </summary>
        public string ProxyTypeName => $"{Type.Name}_proxy_";

        /// <summary>
        /// 獲取代理類的全名。
        /// </summary>
        public string ProxyTypeFullName => $"{Namespace}.{ProxyTypeName}";

        /// <summary>
        /// 獲取源代碼名稱。
        /// </summary>
        public string SourceCodeName => Type.ToDisplayString().Replace(".", "_") + ".cs";

        /// <summary>
        /// 獲取構造函數列表。
        /// </summary>
        public List<IMethodSymbol> Constructors { get; } = new();

        /// <summary>
        /// 獲取可攔截的方法。
        /// </summary>
        public Dictionary<IMethodSymbol, InterceptorMetadata> Methods { get; } = new();

        /// <summary>
        /// 獲取可攔截的屬性。
        /// </summary>
        public Dictionary<IPropertySymbol, InterceptorMetadata> Properties { get; } = new();

        /// <summary>
        /// 獲取引用的命名空間列表。
        /// </summary>
        public List<string> Usings { get; } = new();

        /// <summary>
        /// 添加可攔截的成員。
        /// </summary>
        /// <param name="symbol"></param>
        /// <param name="metadata"></param>
        public void AddMember(ISymbol symbol, InterceptorMetadata metadata)
        {
            if (symbol is IMethodSymbol method)
            {
                Methods.Add(method, metadata);
            }
            else if (symbol is IPropertySymbol property && property.Parameters.Count() == 0) //忽略索引器
            {
                Properties.Add(property, metadata);
            }
        }

        /// <summary>
        /// 添加構造方法。
        /// </summary>
        /// <param name="symbol"></param>
        public void AddConstructor(IMethodSymbol symbol)
        {
            Constructors.Add(symbol);
        }

        /// <summary>
        /// 添加引用的命名空間列表。
        /// </summary>
        /// <param name="usings"></param>
        public void AddUsings(IEnumerable<string> usings)
        {
            Usings.AddRange(usings);
        }

        /// <summary>
        /// 若有可攔截的方法或屬性,則此元數據有效。
        /// </summary>
        public bool IsValid => Methods.Any() || Properties.Any();
    }

  接下來,在 DynamicProxyGenerator 類的 Execute 方法里,獲取到以上記錄到的元數據,分別創建 DynamicProxyClassBuilder 對象來生成代碼,如下:

        void ISourceGenerator.Execute(GeneratorExecutionContext context)
        {
            var mappers = new Dictionary<string, string>();

            if (context.SyntaxContextReceiver is DynamicProxySyntaxReceiver receiver)
            {
                var metadatas = receiver.GetMetadatas();
                metadatas.ForEach(s =>
                {
                    context.AddSource(s.SourceCodeName, new DynamicProxyClassBuilder(s).BuildSource());
                    mappers.Add(s.TypeFullName, s.ProxyTypeFullName);
                });

                //代碼生成完畢後,還需要在部署器中,將父類和代理類添加到 Container 容器中
                if (mappers.Count > 0)
                {
                    context.AddSource("DynamicProxyServicesDeployer.cs", BuildDeploySourceCode(mappers));
                }
            }
        }

  DynamicProxyClassBuilder 類的核心的實現,在這裡,將對構造函數進行重載,對所有的方法、屬性進行切麵代碼的生成。由於篇幅有限,這裡只貼上類的構建方法,其他的對照原型進行參悟,也是比較容易理解的。如下:

        /// <summary>
        /// 生成源代碼。
        /// </summary>
        /// <returns></returns>
        public SourceText BuildSource()
        {
            var sb = new StringBuilder();
            foreach (var u in _metadata.Usings)
            {
                sb.AppendLine(u.ToString());
            }

            sb.AppendLine("using System.Reflection;");

            sb.AppendLine($@"
namespace {_metadata.Namespace}
{{
    public class {_metadata.ProxyTypeName} : {_metadata.TypeFullName}, IDynamicProxyImplemented
    {{
        private List<System.Reflection.MemberInfo> _initMarks = new ();
        
        {BuildConstructors()}
        {BuildInitializeMethod()}
        {BuildInterceptMethod()}
        {BuildMethods()}
        {BuildProperties()}
    }}
}}");

            return SourceText.From(sb.ToString(), Encoding.UTF8);
        }

  那麼同樣的,項目編譯後,會生成一個 __DynamicProxyServicesDeployer 的部署器,目的是往 Container 里添加代理映射。如下:

[assembly: ServicesDeployAttribute(typeof(__DynamicProxyNs.__DynamicProxyServicesDeployer))]

internal class __DynamicProxyServicesDeployer : IServicesDeployer
{
    void IServicesDeployer.Configure(IServiceCollection services)
    {
        Container.TryAdd(typeof(DependencyInjectionTests.TestDynamicProxyClass), typeof(TestDynamicProxyClass_proxy_));
        Container.TryAdd(typeof(DynamicProxyTests.TestProxy), typeof(TestProxy_proxy_));
        Container.TryAdd(typeof(ObjectActivatorTests.TestServiceProxy2), typeof(TestServiceProxy2_proxy_));
    }
}

  單元測試就不再贅述了,可以去這裡查看 動態代理單元測試

  最後,奉上 Fireasy 3 的開源地址:https://gitee.com/faib920/fireasy3 ,歡迎大家前來捧場。

  本文相關代碼請參考:
  https://gitee.com/faib920/fireasy3/src/libraries/Fireasy.Common/DynamicProxy
  https://gitee.com/faib920/fireasy3/src/libraries/Fireasy.Common.Analyzers/DynamicProxy
  https://gitee.com/faib920/fireasy3/tests/Fireasy.Common.Tests/DynamicProxyTests.cs

  更多內容請移步官網 http://www.fireasy.cn 。

作者:fireasy
出處:http://fireasy.cnblogs.com
官網:http://www.fireasy.cn
版權聲明:本文的版權歸作者與博客園共有。轉載時須註明本文的詳細鏈接,否則作者將保留追究其法律責任。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 雙指針 同向雙指針 能夠實現跳躍尋找,適用於尋找含有某一特性區間,比如最長相同區間,最長不重覆區間 不重覆區間可以用一個數組t[N]來表示,如果其中元素大於1,說明有重覆 int res=0,j=0; for(int i=0;i<n;i++) { t[a[i]]++;//記錄個數 while(j<i ...
  • 一、標識符 標識符是指定義的具有特殊意義的詞,例如變數、常量、函數名等等,任何一門語言中都對自己的標識符有特殊定義的規則。在 Go 語言中,標識符由字母數字和下劃線組成,並且只能以字母和下劃線開頭,例如: 數字、字母和下劃線組成:123、abc _ 只能以字母和下劃線開頭:abc123、_sysVa ...
  • 靜態類和非靜態類 靜態類只能是內部類,外部類不能用static修飾,內部類可以使用static修飾。 創建方式: 外部類.靜態內部類 對象名=外部類.new 靜態內部類(); 外部類 對象名= new 外部類(); 外部類.非靜態內部類 對象名1 = 對象名.new 非靜態內部類(); 抽象類是否可 ...
  • 1.java開發工具的構成 (1)JVM即java虛擬機,用於解釋翻譯java生成的自解碼文件,因為java中有了這個,才能實現java語言跨平臺運行java自解碼文件 (2)JRE即java運行環境,它是由JVM和java系統類庫構成,JRE是能保證java代碼能夠準確無誤在裝了JRE機器上運行, ...
  • 本文已經收錄到Github倉庫,該倉庫包含電腦基礎、Java基礎、多線程、JVM、資料庫、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分散式、微服務、設計模式、架構、校招社招分享等核心知識點,歡迎star~ Github地址:https://github.c ...
  • 需要使用的 技術 工具: idea 2022 開發工具 MySql 5.6 資料庫工具 Apache Tomcat 8.5.85 web應用部署工具 主要功能有: 用戶登錄 用戶註冊 展示列表信息 涉及到的知識: Servlet JDBC 過濾器 先打開資料庫 創建一個資料庫 然後 創建對應的表結構 ...
  • 可讀、可寫 r+t: 可讀、可寫 w+t: 可寫、可讀 a+t: 可追加、可讀 ## wt with open('36w.txt', 'wt', encoding='utf-8') as fw: print(fw.readable()) print(fw.writable()) False True ...
  • 1.項目 https://github.com/Fody/Costura 2.安裝 Install-Package Costura.Fody -Version 1.6.2 3.反射相關 Assembly.LoadFrom("someLibs.dll").GetType("xxx") 找不到文件 改為 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...