攜程 Apollo 配置中心傳統 .NET 專案整合實踐

Esofar發表於2019-08-07

攜程 Apollo 配置中心傳統 .NET 專案整合實踐

官方文件存在的問題

可能由於 Apollo 配置中心的客戶端原始碼一直處於更新中,導致其相關文件有些跟不上節奏,部分文件寫的不規範,很容易給做對接的新手朋友造成誤導。

比如,我在參考如下兩個文件使用傳統 .NET 客戶端做接入的時候就發現了些問題。

1.兩個文件關於標識應用身份的AppId的配置節點不一致。
攜程 Apollo 配置中心傳統 .NET 專案整合實踐
攜程 Apollo 配置中心傳統 .NET 專案整合實踐

2.第二個文件關於應用配置釋出環境的Environment配置節點的描述出現明顯錯誤。
攜程 Apollo 配置中心傳統 .NET 專案整合實踐

當然,這些問題隨時都有可能被修復。若您看到文件內容與本文描述不符,請以官方文件為準。

傳統 .NET 專案快速接入

快速進入正題。

安裝依賴包

在您專案的基礎設施層,通過 NuGet 包管理器或使用如下命令新增傳統 .NET 專案使用的客戶端:

Install-Package Com.Ctrip.Framework.Apollo.ConfigurationManager -Version 2.0.3

從上面的包名能看出什麼?我這裡選裝的是2.0.3的版本。還有,這應該是一個 Javaer 起的名字。

配置應用標識 & 服務地址

在您的啟動專案中,開啟App.configWeb.config配置檔案,在<appSettings>節點中增加如下節點:

<!-- Change to the actual app id -->
<add key="Apollo.AppID" value="R01001" />
<add key="Apollo.MetaServer" value="http://localhost:8080" />

若您部署了多套 Config Service,支援多環境,請參考如下配置:

<!-- Change to the actual app id -->
<add key="Apollo.AppID" value="R01001" />

<!-- Should change the apollo config service url for each environment -->
<add key="Apollo.Env" value="DEV" />
<add key="Apollo.DEV.Meta" value="http://localhost:8080"/>
<add key="Apollo.FAT.Meta" value="http://localhost:8081"/>
<add key="Apollo.UAT.Meta" value="http://localhost:8082"/>
<add key="Apollo.PRO.Meta" value="http://localhost:8083"/>

配置完成後,就可以準備在我們專案中使用 Apollo 客戶端了。

二次封裝程式碼

我們習慣在專案中使用第三方庫的時候封裝一層,這種封裝是淺層的,一般都是在專案的基礎設施層來做,這樣其他層使用就不需要再次引入依賴包。

不說了,直接上程式碼吧。

程式碼結構大致如下:

├─MyCompany.MyProject.Infrastructure         # 專案基礎設施層
│  │                                                       
│  └─Configuration                         
│          ApolloConfiguration.cs            # Apollo 分散式配置項讀取實現     
│          ConfigurationChangeEventArgs.cs   # 配置更改回撥事件引數
│          IConfiguration.cs                 # 配置抽象介面,可基於此介面實現本地配置讀取

IConfiguration

using System;
using System.Configuration;

namespace MyCompany.MyProject.Infrastructure
{
    /// <summary>
    /// 配置抽象介面。
    /// </summary>
    public interface IConfiguration
    {
        /// <summary>
        /// 配置更改回撥事件。
        /// </summary>
        event EventHandler<ConfigurationChangeEventArgs> ConfigChanged;

        /// <summary>
        /// 獲取配置項。
        /// </summary>
        /// <param name="key">鍵</param>
        /// <param name="namespaces">名稱空間集合</param>
        /// <returns></returns>
        string GetValue(string key, params string[] namespaces);

        /// <summary>
        /// 獲取配置項。
        /// </summary>
        /// <typeparam name="TValue">值型別</typeparam>
        /// <param name="key">鍵</param>
        /// <param name="namespaces">名稱空間集合</param>
        /// <returns></returns>
        TValue GetValue<TValue>(string key, params string[] namespaces);

        /// <summary>
        /// 獲取配置項,如果值為 <see cref="null"/> 則取引數 <see cref="defaultValue"/> 值。
        /// </summary>
        /// <param name="key">鍵</param>
        /// <param name="defaultValue">預設值</param>
        /// <param name="namespaces">名稱空間集合</param>
        /// <returns></returns>
        string GetDefaultValue(string key, string defaultValue, params string[] namespaces);

        /// <summary>
        /// 獲取配置項,如果值為 <see cref="null"/> 則取引數 <see cref="defaultValue"/> 值。
        /// </summary>
        /// <typeparam name="TValue">值型別</typeparam>
        /// <param name="key">鍵</param>
        /// <param name="defaultValue">預設值</param>
        /// <param name="namespaces">名稱空間集合</param>
        /// <returns></returns>
        TValue GetDefaultValue<TValue>(string key, TValue defaultValue, params string[] namespaces);
    }
}

ConfigurationChangeEventArgs

using Com.Ctrip.Framework.Apollo.Model;
using System.Collections.Generic;

namespace MyCompany.MyProject.Infrastructure
{
    public class ConfigurationChangeEventArgs
    {
        public IEnumerable<string> ChangedKeys => Changes.Keys;
        public bool IsChanged(string key) => Changes.ContainsKey(key);
        public string Namespace { get; }
        public IReadOnlyDictionary<string, ConfigChange> Changes { get; }
        public ConfigurationChangeEventArgs(string namespaceName, IReadOnlyDictionary<string, ConfigChange> changes)
        {
            Namespace = namespaceName;
            Changes = changes;
        }
        public ConfigChange GetChange(string key)
        {
            Changes.TryGetValue(key, out var change);
            return change;
        }
    }
}

ApolloConfiguration

using System;
using System.Configuration;
using System.Globalization;
using Com.Ctrip.Framework.Apollo;
using Com.Ctrip.Framework.Apollo.Model;

namespace MyCompany.MyProject.Infrastructure
{
    public class ApolloConfiguration : IConfiguration
    {
        private readonly string _defaultValue = null;

        public event EventHandler<ConfigurationChangeEventArgs> ConfigChanged;

        private IConfig GetConfig(params string[] namespaces)
        {
            var config = namespaces == null || namespaces.Length == 0 ?
                ApolloConfigurationManager.GetAppConfig().GetAwaiter().GetResult() :
                ApolloConfigurationManager.GetConfig(namespaces).GetAwaiter().GetResult();

            config.ConfigChanged += (object sender, ConfigChangeEventArgs args) =>
            {
                ConfigChanged(sender, new ConfigurationChangeEventArgs(args.Namespace, args.Changes));
            };

            return config;
        }

        public string GetValue(string key, params string[] namespaces)
        {
            key = key ?? throw new ArgumentNullException(nameof(key));
            var config = GetConfig(namespaces);
            return config.GetProperty(key, _defaultValue);
        }

        public TValue GetValue<TValue>(string key, params string[] namespaces)
        {
            var value = GetValue(key, namespaces);
            return value == null ?
                default(TValue) :
                (TValue)Convert.ChangeType(value, typeof(TValue), CultureInfo.InvariantCulture);
        }

        public string GetDefaultValue(string key, string defaultValue, params string[] namespaces)
        {
            key = key ?? throw new ArgumentNullException(nameof(key));
            var config = GetConfig(namespaces);
            return config.GetProperty(key, defaultValue);
        }

        public TValue GetDefaultValue<TValue>(string key, TValue defaultValue, params string[] namespaces)
        {
            var value = GetDefaultValue(key, defaultValue, namespaces);
            return value == null ?
                default(TValue) :
                (TValue)Convert.ChangeType(value, typeof(TValue), CultureInfo.InvariantCulture);
        }
    }
}

正確使用姿勢

在使用之前需要先把ApolloConfiguration註冊到應用容器中,請參考如下程式碼:

// 這裡我們專案使用的 DI 框架是`Autofac`,按需修改吧,記得將例項註冊成單例模式。
public class DependencyRegistrar : IDependencyRegistrar
{
    public void Register(ContainerBuilder builder, ITypeFinder typeFinder)
    {
        
        builder.RegisterType<ApolloConfiguration>()
            .As<IConfiguration>()
            .Named<IConfiguration>("configuration")
            .SingleInstance();
            
        ...
    }

    public int Order
    {
        get { return 1; }
    }
}

接下來就可以在專案中使用了,請參考如下程式碼:

public class UserController : BaseController
{
    private readonly IConfiguration _configuration;

    public UserController(IConfiguration configuration)
    {
        _configuration = configuration;
    }
    
    public ActionResult Add(AddUserInput model)
    {
        if (ModelState.IsValid)
        {
            // 從 Apollo 分散式配置中心 專案`R01001` 預設名稱空間`application`下 讀取配置項。
            model.Password = _configuration.GetValue("DefaultUserPassword");
            ...
        }
        ...
    }
}

相關文章