携程 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 UserApiController(IConfiguration configuration)
    {
        _configuration = configuration;
    }
    
    public ActionResult Add(AddUserInput model)
    {
        if (ModelState.IsValid)
        {
            // 从 Apollo 分布式配置中心 项目`R01001` 默认命名空间`application`下 读取配置项。
            model.Password = _configuration.GetValue("DefaultUserPassword");
            ...
        }
        ...
    }
}
上一篇:分布式配置中心Apollo


下一篇:第七章 百度Apollo感知介绍(1)