ASP.NET Core 3.0 : 二十四. 配置的Options模式

来源:https://www.cnblogs.com/FlyLolo/archive/2019/09/27/ASPNETCore_24.html
-Advertisement-
Play Games

上一章講到了配置的用法及內部處理機制,對於配置,ASP.NET Core還提供了一種Options模式。(ASP.NET Core 系列目錄) 一、Options的使用 上一章有個配置的綁定的例子,可以將配置綁定到一個Theme實例中。也就是在使用對應配置的時候,需要進行一次綁定操作。而Option ...


上一章講到了配置的用法及內部處理機制,對於配置,ASP.NET Core還提供了一種Options模式。(ASP.NET Core 系列目錄)

一、Options的使用

上一章有個配置的綁定的例子,可以將配置綁定到一個Theme實例中。也就是在使用對應配置的時候,需要進行一次綁定操作。而Options模式提供了更直接的方式,並且可以通過依賴註入的方式提供配置的讀取。下文中稱每一條Options配置為Option。

1.簡單的不為Option命名的方式

依然採用這個例子,在appsettings.json中存在這樣的配置:

{
  "Theme": {
    "Name": "Blue",
    "Color": "#0921DC"
  }
}

修改一下ValueController,代碼如下:

public class ValuesController : Controller
{
    private IOptions<Theme> _options = null;
    public ValuesController(IOptions<Theme> options)
    {
        _options = options;
    }

    public ContentResult GetOptions()
    {
        return new ContentResult() { Content = $"options:{ _options.Value.Name}" };
    }
}

依然需要在Startup文件中做註冊:

    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<Theme>(Configuration.GetSection("Theme"));

        services.AddControllersWithViews();  //3.0中啟用的新方法
    }

 

請求這個Action,獲取到的結果為:

options:Blue

這樣就可以在需要使用該配置的時候通過依賴註入的方式使用了。但有個疑問,這裡將“Theme”類型綁定了這樣的配置,但如果有多個這樣的配置呢?就如同下麵這樣的配置的時候:

  "Themes": [
    {
      "Name": "Blue",
      "Color": "#0921DC"
    },
    {
      "Name": "Red",
      "Color": "#FF4500"
    }
  ]

在這樣的情況下,存在多個Theme的配置,這樣對於之前這種依賴註入的方式就不行了。這時系統提供了將註入的Options進行命名的方法。

2.為Option命名的方式

首先需要在Startup文件中註冊的時候對其命名,添加如下兩條註冊代碼:

services.Configure<Theme>("ThemeBlue", Configuration.GetSection("Themes:0"));
services.Configure<Theme>("ThemeRed" , Configuration.GetSection("Themes:1"));

修改ValueController代碼,添加IOptionsMonitor<Theme>和IOptionsSnapshot<Theme>兩種新的註入方式如下:

        private IOptions<Theme> _options = null;
        private IOptionsMonitor<Theme> _optionsMonitor = null;
        private IOptionsSnapshot<Theme> _optionsSnapshot = null;
        public ValuesController(IOptions<Theme> options, IOptionsMonitor<Theme> optionsMonitor, IOptionsSnapshot<Theme> optionsSnapshot)
        {
            _options = options;
            _optionsMonitor = optionsMonitor;
            _optionsSnapshot = optionsSnapshot;
        }

        public ContentResult GetOptions()
        {
            return new ContentResult() { Content = $"options:{_options.Value.Name}," +
                $"optionsSnapshot:{ _optionsSnapshot.Get("ThemeBlue").Name }," +
                $"optionsMonitor:{_optionsMonitor.Get("ThemeRed").Name}" };
        }

請求這個Action,獲取到的結果為:

options:Blue,optionsSnapshot:Red,optionsMonitor:Gray

新增的兩種註入方式通過Options的名稱獲取到了對應的Options。為什麼是兩種呢?它們有什麼區別?不知道有沒有讀者想到上一章配置的重新載入功能。在配置註冊的時候,有個reloadOnChange選項,如果它被設置為true的,當對應的數據源發生改變的時候,會進行重新載入。而Options怎麼能少了這樣的特性呢。

3.Option的自動更新與生命周期

為了驗證這三種Options的讀取方式的特性,修改Theme類,添加一個Guid欄位,併在構造方法中對其賦值,代碼如下:

public class Theme
{
    public Theme()
    {
        Guid = Guid.NewGuid();
    }
    public Guid Guid { get; set; }
    public string Name { get; set; }
    public string Color { get; set; }
}

修改上例中的名為GetOptions的Action的代碼如下:

public ContentResult GetOptions()
{
    return new ContentResult()
    {
        Content = $"options:{_options.Value.Name}|{_options.Value.Guid}," +
        $"optionsSnapshot:{ _optionsSnapshot.Get("ThemeBlue").Name }|{_optionsSnapshot.Get("ThemeBlue").Guid}," +
        $"optionsMonitor:{_optionsMonitor.Get("ThemeRed").Name}|{_optionsMonitor.Get("ThemeRed").Guid}"
    };
}

請求這個Action,返回結果如下:

options:Blue|ad328f15-254f-4505-a79f-4f27db4a393e,optionsSnapshot:Red|dba5f550-29ca-4779-9a02-781dd17f595a,optionsMonitor:Gray|a799fa41-9444-45dd-b51b-fcd15049f98f

刷新頁面,返回結果為:

options:Blue|ad328f15-254f-4505-a79f-4f27db4a393e,optionsSnapshot:Red|a2350cb3-c156-4f71-bb2d-25890fe08bec,optionsMonitor:Gray|a799fa41-9444-45dd-b51b-fcd15049f98f

可見IOptions和IOptionsMonitor兩種方式獲取到的Name值和Guid值均未改變,而通過IOptionsSnapshot方式獲取到的Name值未改變,但Guid值發生了改變,每次刷新頁面均會改變。這類似前面講依賴註入時做測試的例子,現在猜測Guid未改變的IOptions和IOptionsMonitor兩種方式是採用了Singleton模式,而Guid發生改變的IOptionsSnapshot方式是採用了Scoped或Transient模式。如果在這個Action中多次採用IOptionsSnapshot讀取_optionsSnapshot.Get("ThemeBlue").Guid的值,會發現同一次請求的值是相同的,不同請求之間的值是不同的,也就是IOptionsSnapshot方式使採用了Scoped模式(此驗證示例比較簡單,請讀者自行修改代碼驗證)。

在這樣的情況下,修改三種獲取方式對應的配置項的Name值,例如分別修改為“Blue1”、“Red1”和“Gray1”,再次多次刷新頁面查看返回值,會發現如下情況:

IOptions方式:Name和Guid的值始終未變。Name值仍為Blue。

IOptionsSnapshot方式:Name值變為Red1,Guid值單次請求內相同,每次刷新之間不同。

IOptionsMonitor方式:只有修改配置值後第一次刷新的時候將Name值變為了Gray1,Guid未改變。之後多次刷新,這兩個值均未做改變。

總結:IOptions和IOptionsMonitor兩種方式採用了Singleton模式,但區別在於IOptionsMonitor會監控對應數據源的變化,如果發生了變化則更新實例的配置值,但不會重新提供新的實例。IOptionsSnapshot方式採用了Scoped模式每次請求採用同一個實例,在下一次請求的時候獲取到的是一個新的實例,所以如果數據源發生了改變,會讀取到新的值。先大概記一下這一的情況,在下文剖析IOptions的內部處理機制的時候就會明白為什麼會這樣。

4.數據更新提醒

IOptionsMonitor方式還提供了一個OnChange方法,當數據源發生改變的時候會觸發它,所以如果想在這時候做點什麼,可以利用這個方法實現。示例代碼:

_optionsMonitor.OnChange((theme,name)=> { Console.WriteLine(theme.Name +"-"+ name); });

5.不採用Configuration配置作為數據源的方式

上面的例子都是採用了讀取配置的方式,實際上Options模式和上一章的Configuration配置方式使分開的,讀取配置只不過是Options模式的一種實現方式,例如可以不使用Configuration中的數據,直接通過如下代碼註冊:

services.Configure<Theme>("ThemeBlack", theme => {
    theme.Color = "#000000";
    theme.Name = "Black";
}); 

6.ConfigureAll方法

系統提供了一個ConfigureAll方法,可以將所有對應的實例統一設置。例如如下代碼:

services.ConfigureAll<Theme>(theme => {
     theme.Color = "#000000";
     theme.Name = "Black2";
});

此時無論通過什麼名稱去獲取Theme的實例,包括不存在對應設置的名稱,獲取到的值都是本次通過ConfigureAll設置的值。

7.PostConfigure和PostConfigureAll方法

這兩個方法和Configure、ConfigureAll方法類似,只是它們會在Configure、ConfigureAll之後執行。

8.多個Configure、ConfigureAll、PostConfigure和PostConfigureAll的執行順序

可以這樣理解,每個Configure都是去修改一個名為其設置的名稱的變數,以如下代碼為例:

services.Configure<Theme>("ThemeBlack", theme => {
    theme.Color = "#000000";
    theme.Name = "Black";
}); 

這條設置就是去修改(註意是修改而不是替換)一個名為"ThemeBlack"的Theme類型的變數,如果該變數不存在,則創建一個Theme實例並賦值。這樣就生成了一些變數名為“空字元串、“ThemeBlue”、“ThemeBlack”的變數(只是舉例,忽略空字元串作為變數名不合法的顧慮)”。

依次按照代碼的順序執行,這時候如果後面的代碼中出現同名的Configure,則修改對應名稱的變數的值。如果是ConfigureAll方法,則修改所有類型為Theme的變數的值。

而PostConfigure和PostConfigureAll則在Configure和ConfigureAll之後執行,即使Configure的代碼寫在了PostConfigure之後也是一樣。

至於為什麼會是這樣的規則,下一節會詳細介紹。

二、內部處理機制解析

1. 系統啟動階段,依賴註入

上一節的例子中涉及到了三個介面IOptions、IOptionsSnapshot和IOptionsMonitor,那麼就從這三個介面說起。既然Options模式是通過這三個介面的泛型方式註入提供服務的,那麼在這之前系統就需要將它們對應的實現註入到依賴註入容器中。這發生在系統啟動階段創建IHost的時候,這時候HostBuilder的Build方法中調用了一個services.AddOptions()方法,這個方法定義在OptionsServiceCollectionExtensions中,代碼如下:

public static class OptionsServiceCollectionExtensions
    {
        public static IServiceCollection AddOptions(this IServiceCollection services)
        {
            if (services == null)
            {
                throw new ArgumentNullException(nameof(services));
            }

            services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));
            services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));
            services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));
            services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));
            services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));
            return services;
        }

        public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class
            => services.Configure(Options.Options.DefaultName, configureOptions);

        public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions)
            where TOptions : class
        {
            //省略非空驗證代碼

            services.AddOptions();
            services.AddSingleton<IConfigureOptions<TOptions>>(new ConfigureNamedOptions<TOptions>(name, configureOptions));
            return services;
        }

        public static IServiceCollection ConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions) where TOptions : class
            => services.Configure(name: null, configureOptions: configureOptions);
//省略部分代碼
    }

可見這個AddOptions方法的作用就是進行服務註入,IOptions<>、IOptionsSnapshot<>對應的實現是OptionsManager<>,只是分別採用了Singleton和Scoped兩種生命周期模式,IOptionsMonitor<>對應的實現是OptionsMonitor<>,同樣為Singleton模式,這也驗證了上一節例子中的猜想。除了上面提到的三個介面外,還有IOptionsFactory<>和IOptionsMonitorCache<>兩個介面,這也是Options模式中非常重要的兩個組成部分,接下來的內容中會用到。

另外的兩個Configure方法就是上一節例子中用到的將具體的Theme註冊到Options中的方法了。二者的區別就是是否為配置的option命名,而第一個Configure方法就未命名的方法,通過上面的代碼可知它實際上是傳入了一個預設的Options.Options.DefaultName作為名稱,這個預設值是一個空字元串,也就是說,未命名的Option相當於是被命名為空字元串。最終都是按照已命名的方式也就是第二個Configure方法進行處理。還有一個ConfigureAll方法,它是傳入了一個null作為Option的名稱,也是交由第二個Configure處理。

在第二個Configure方法中仍調用了一次AddOptions方法,然後才是將具體的類型進行註入。AddOptions方法中採用的都是TryAdd方法進行註入,已被註入的不會被再次註入。接下來註冊了一個IConfigureOptions<TOptions>介面,對應的實現是ConfigureNamedOptions<TOptions>(name, configureOptions),它的代碼如下:

public class ConfigureNamedOptions<TOptions> : IConfigureNamedOptions<TOptions> where TOptions : class
{
    public ConfigureNamedOptions(string name, Action<TOptions> action)
    {
        Name = name;
        Action = action;
}

    public string Name { get; }
    public Action<TOptions> Action { get; }

    public virtual void Configure(string name, TOptions options)
    {
        if (options == null)
        {
            throw new ArgumentNullException(nameof(options));
        }

        // Null name is used to configure all named options.
        if (Name == null || name == Name)
        {
            Action?.Invoke(options);
        }
    }

    public void Configure(TOptions options) => Configure(Options.DefaultName, options);
}

它在構造方法中存儲了配置的名稱(Name)和創建方法(Action),它的兩個Configure方法用於在獲取Options的值的時候執行對應的Action來創建實例(例如示例中的Theme)。在此時不會被執行。所以在此會出現3中類型的ConfigureNamedOptions,分別是Name值為具體值的、Name值為為空字元串的和Name值為null的。這分別對應了第一節的例子中的為Option命名的Configure方法、不為Option命名的Configure方法、以及ConfigureAll方法。

此處用到的OptionsServiceCollectionExtensions和ConfigureNamedOptions對應的是通過代碼直接註冊Option的方式,例如第一節例子中的如下方式:

services.Configure<Theme>("ThemeBlack", theme => { new Theme { Color = "#000000", Name = "Black" }; });

如果是以Configuration作為數據源的方式,例如如下代碼

services.Configure<Theme>("ThemeBlue", Configuration.GetSection("Themes:0"));

用到的是OptionsServiceCollectionExtensions和ConfigureNamedOptions這兩個類的子類,分別為OptionsConfigurationServiceCollectionExtensions和NamedConfigureFromConfigurationOptions兩個類,通過它們的名字也可以知道是專門用於採用Configuration作為數據源用的,代碼類似,只是多了一條關於IOptionsChangeTokenSource的依賴註入,作用是將Configuration的關於數據源變化的監聽和Options的關聯起來,當數據源發生改變的時候可以及時更新Options中的值,主要的Configure方法代碼如下:

public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, IConfiguration config, Action<BinderOptions> configureBinder)
    where TOptions : class
{
    //省略驗證代碼

    services.AddOptions();
    services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(new ConfigurationChangeTokenSource<TOptions>(name, config));
    return services.AddSingleton<IConfigureOptions<TOptions>>(new NamedConfigureFromConfigurationOptions<TOptions>(name, config, configureBinder));
}

同樣還有PostConfigure和PostConfigureAll方法,和Configure、ConfigureAll方法類似,只不過註入的類型為IPostConfigureOptions<TOptions>。

2. Options值的獲取

Option值的獲取也就是從依賴註入容器中獲取相應實現的過程。通過依賴註入階段,已經知道了IOptions<>和IOptionsSnapshot<>對應的實現是OptionsManager<>,就以OptionsManager<>為例看一下依賴註入後的服務提供過程。OptionsManager<>代碼如下:

public class OptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class, new()
{
    private readonly IOptionsFactory<TOptions> _factory;
private readonly OptionsCache<TOptions> _cache = new OptionsCache<TOptions>();

    public OptionsManager(IOptionsFactory<TOptions> factory)
    {
        _factory = factory;
    }

    public TOptions Value
    {
        get
        {
            return Get(Options.DefaultName);
        }
    }

    public virtual TOptions Get(string name)
    {
        name = name ?? Options.DefaultName;
        return _cache.GetOrAdd(name, () => _factory.Create(name));
    }
}

它有IOptionsFactory<TOptions>和OptionsCache<TOptions>兩個重要的成員。如果直接獲取Value值,實際上是調用的另一個Get(string name)方法,傳入了空字元串作為name值。所以最終值的獲取還是在緩存中讀取,這裡的代碼是_cache.GetOrAdd(name, () => _factory.Create(name)),即如果緩存中存在對應的值,則返回,如果不存在,則由_factory去創建。OptionsFactory<TOptions>的代碼如下:

public class OptionsFactory<TOptions> : IOptionsFactory<TOptions> where TOptions : class, new()
{
    private readonly IEnumerable<IConfigureOptions<TOptions>> _setups;
    private readonly IEnumerable<IPostConfigureOptions<TOptions>> _postConfigures;
    private readonly IEnumerable<IValidateOptions<TOptions>> _validations;

    public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures) : this(setups, postConfigures, validations: null)
    { }

    public OptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations)
    {
        _setups = setups;
        _postConfigures = postConfigures;
        _validations = validations;
}

    public TOptions Create(string name)
    {
        var options = new TOptions();
        foreach (var setup in _setups)
        {
            if (setup is IConfigureNamedOptions<TOptions> namedSetup)
            {
                namedSetup.Configure(name, options);
            }
            else if (name == Options.DefaultName)
            {
                setup.Configure(options);
            }
        }
        foreach (var post in _postConfigures)
        {
            post.PostConfigure(name, options);
        }

        if (_validations != null)
        {
            var failures = new List<string>();
            foreach (var validate in _validations)
            {
                var result = validate.Validate(name, options);
                if (result.Failed)
                {
                    failures.AddRange(result.Failures);
                }
            }
            if (failures.Count > 0)
            {
                throw new OptionsValidationException(name, typeof(TOptions), failures);
            }
        }

        return options;
    }
}

主要看它的TOptions Create(string name)方法。這裡會遍歷它的_setups集合,這個集合類型為IEnumerable<IConfigureOptions<TOptions>>,在講Options模式的依賴註入的時候已經知道,每一個Configure、ConfigureAll實際上就是向依賴註入容器中註冊了一個IConfigureOptions<TOptions>,只是名稱可能不同。而PostConfigure和PostConfigureAll方法註冊的是IPostConfigureOptions<TOptions>類型,對應的就是_postConfigures集合。

首先會遍歷_setups集合,調用IConfigureOptions<TOptions>的Configure方法,這個方法的主要代碼就是:

 if (Name == null || name == Name)
 {
      Action?.Invoke(options);
 }

如果Name值為null,即對應的是ConfigureAll方法,則執行該Action。或者Name值和需要讀取的值相同,則執行該Action。

_setups集合遍歷之後,同樣的機制遍歷_postConfigures集合。這就是上一節關於Configure、ConfigureAll、PostConfigure和PostConfigureAll的執行順序的驗證。

最終返回對應的實例並寫入緩存。這就是IOptions和IOptionsSnapshot兩種模式的處理機制,接下來看一下IOptionsMonitor模式,它對應的實現是OptionsMonitor。代碼如下:

public class OptionsMonitor<TOptions> : IOptionsMonitor<TOptions> where TOptions : class, new()
{
    private readonly IOptionsMonitorCache<TOptions> _cache;
    private readonly IOptionsFactory<TOptions> _factory;
    private readonly IEnumerable<IOptionsChangeTokenSource<TOptions>> _sources;
    internal event Action<TOptions, string> _onChange;

    public OptionsMonitor(IOptionsFactory<TOptions> factory, IEnumerable<IOptionsChangeTokenSource<TOptions>> sources, IOptionsMonitorCache<TOptions> cache)
    {
        _factory = factory;
        _sources = sources;
        _cache = cache;

        foreach (var source in _sources)
        {
                var registration = ChangeToken.OnChange(
                      () => source.GetChangeToken(),
                      (name) => InvokeChanged(name),
                      source.Name);

                _registrations.Add(registration);        
}
    }

    private void InvokeChanged(string name)
    {
        name = name ?? Options.DefaultName;
        _cache.TryRemove(name);
        var options = Get(name);
        if (_onChange != null)
        {
            _onChange.Invoke(options, name);
        }
    }

    public TOptions CurrentValue
    {
        get => Get(Options.DefaultName);
    }

    public virtual TOptions Get(string name)
    {
        name = name ?? Options.DefaultName;
        return _cache.GetOrAdd(name, () => _factory.Create(name));
    }

    public IDisposable OnChange(Action<TOptions, string> listener)
    {
        var disposable = new ChangeTrackerDisposable(this, listener);
        _onChange += disposable.OnChange;
        return disposable;
    }

    internal class ChangeTrackerDisposable : IDisposable
    {
        private readonly Action<TOptions, string> _listener;
        private readonly OptionsMonitor<TOptions> _monitor;

        public ChangeTrackerDisposable(OptionsMonitor<TOptions> monitor, Action<TOptions, string> listener)
        {
            _listener = listener;
            _monitor = monitor;
        }

        public void OnChange(TOptions options, string name) => _listener.Invoke(options, name);

        public void Dispose() => _monitor._onChange -= OnChange;
    }
}

大部分功能和OptionsManager類似,只是由於它是採用了Singleton模式,所以它是採用監聽數據源改變並更新的模式。當通過Configuration作為數據源註冊Option的時候,多了一條IOptionsChangeTokenSource的依賴註入。當數據源發生改變的時候更新數據並觸發OnChange(Action<TOptions, string> listener),在第一節的數據更新提醒中有相關的例子。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這邏輯,強無敵! 只要你載入一次,就別想再載入第二次。 ...
  • <Grid HorizontalAlignment="Center" VerticalAlignment="Center"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition/> </Grid.RowDefinit... ...
  • 前提 入行已經7,8年了,一直想做一套漂亮點的自定義控制項,於是就有了本系列文章。 GitHub:https://github.com/kwwwvagaa/NetWinformControl 碼雲:https://gitee.com/kwwwvagaa/net_winform_custom_contr ...
  • 今天寫代碼發現有些代碼行參差不齊,空行又多,整理看起來醜的不行,於是上網搜了下代碼整理的快捷方式以作記錄 這是整理之前,亂糟糟的(故意打亂為了節目效果) 第一步:Ctrl+a (全選代碼) 第二步:Ctrl+K+F(整理代碼) 是不是整齊多了,但是你以為這樣就完了嗎?不不不,這些空行難道不醜嗎? 第 ...
  • System.Text.Json處理Json文檔需要用到JsonDocument,JsonElement,JsonProperty。 JsonDocument代表一個Json文檔,JsonElement就是Json的元素集合。 處理Json文檔時基本是對JsonElement和JsonPropert ...
  • 最近遇到一個.NET連接Oracle的一個錯誤,其主要原因是換了一臺電腦,在新電腦上運行以前的項目出現了的一個錯誤,工作環境為vs2017+Oracle 64位,win10系統 這個錯誤頭疼了一天,找了好多博客去解決這個問題 在這主要是總結一下本人的解決思路與方法。 1.查看自己的Oracle客戶端 ...
  • 前言 OAuth 2.0預設四種授權模式(GrantType) 授權碼模式(authorization_code) 簡化模式(implicit) 密碼模式(resource owner password credentials) "客戶端模式(client_credentials)" 本章主要介紹客 ...
  • [toc] 前言 本來打算昨天都開始寫這篇,就因為要把小團隊的博客整理彙總,一看二哈的博客那麼多,一個個複製粘貼肯定麻煩(其實是我自己覺得複製麻煩),所以穿插著寫了個小爬蟲,後續寫差不多了就拿出來晾晾吧(py菜雞水平)。 之前開發的時候,忽略了記錄,等到想寫點兒啥跟後臺有關的東西的時候,還得一點點回 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...