如何合并多个 ASP.Net Core 配置文件中的数组?

7

我希望在我的应用程序中动态加载和注册服务。为了做到这一点,我需要能够从解决方案中的不同项目中加载配置文件,并将它们的值合并到单个 JSON 数组中。

不幸的是,默认情况下,在 ASP.NET Core 配置中会覆盖值。

我使用以下代码(Program.cs 文件的一部分)来注册文件:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((webHostBuilderContext, configurationbuilder) =>
            {
                var env = webHostBuilderContext.HostingEnvironment;
                configurationbuilder.SetBasePath(env.ContentRootPath);
                configurationbuilder.AddJsonFile("appsettings.json", false, true);

                var path = Path.Combine(env.ContentRootPath, "App_Config\\Include");

                foreach(var file in Directory.EnumerateFiles(path, "*.json",SearchOption.AllDirectories))
                {
                    configurationbuilder.AddJsonFile(file, false, true);
                }
                configurationbuilder.AddEnvironmentVariables();
            })
            .UseStartup<Startup>();

这段代码搜索 App_Config\Include 目录下所有扩展名为 *.json 的文件,并将它们全部添加到配置生成器中。

文件的结构如下:

{
  "ServicesConfiguration": {
    "Services": [
      {
        "AssemblyName": "ParsingEngine.ServicesConfigurator, ParsingEngine"
      }
    ]
  }
}

正如您所看到的,我有一个名为ServicesConfiguration的主要部分,然后是包含对象的Services数组,这些对象具有一个属性AssemblyName

为了读取这些值,我使用带列表的ServicesConfiguration类:

public class ServicesConfiguration
    {
        public List<ServiceAssembly> Services { get; set; }
    }

那个列表使用 ServiceAssembly 类:

public class ServiceAssembly
    {
        public string AssemblyName { get; set; }
    }

为了加载该配置,我在构造函数级别(DI)使用 IOptions
Microsoft.Extensions.Options.IOptions<ServicesConfiguration> servicesConfiguration,

看起来配置已经加载 - 但是从文件中读取的值没有合并,而是被最后找到的文件覆盖了。

有什么想法可以解决这个问题吗?


嗯...我认为它可能会起作用的一种方式(虽然肯定不是最美观的)是:您可以自己合并json文件(获取文件,解析它并放在某个地方),进行合并,最终得到您想要的List<ServiceAssembly> Services,然后再将其序列化为json文件,并调用AddJson传递它。 - jpgrassi
思考得更好,你甚至可以忘记添加JSON并使用“传统”方式。你只需要自己读取JSON文件,解析它们成类,然后将该类注册为单例。这样一来,你就可以随心所欲地处理JSON文件,而且还避免了使用IOptions模式,这是大多数人(包括我在内)都不喜欢的。 - jpgrassi
2个回答

5

如果你想知道我在评论中所说的意思,这里有一个潜在的答案:

由于你需要从不同的项目中加载不同的“配置”文件并应用一些合并逻辑,我建议避免使用“默认”配置系统将JSON文件加载到应用程序中。相反,我会自己处理它。所以:

  1. 读取和反序列化JSON为类型,并将其保存在列表中
  2. 浏览包含所有配置的列表并应用您的合并逻辑
  3. 将单个 ServicesConfiguration 注册为 Singleton
  4. 删除你在 Program.cs 中加载自定义 JSON 文件的代码

以下是如何实现:

ServicesRootConfiguration (新类,以便能够反序列化json)

public class ServicesRootConfiguration
{
    public ServicesConfiguration ServicesConfiguration { get; set; }
}

Startup.cs

public class Startup
{
    private readonly IHostingEnvironment _hostingEnvironment;

    public Startup(IConfiguration configuration, IHostingEnvironment env)
    {
        Configuration = configuration;
        _hostingEnvironment = env;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        // other configuration omitted for brevity

        // build your custom configuration from json files
        var myCustomConfig = BuildCustomConfiguration(_hostingEnvironment);

        // Register the configuration as a Singleton
        services.AddSingleton(myCustomConfig);
    }

    private static ServicesConfiguration BuildCustomConfiguration(IHostingEnvironment env)
    {
        var allConfigs = new List<ServicesRootConfiguration>();

        var path = Path.Combine(env.ContentRootPath, "App_Config");

        foreach (var file in Directory.EnumerateFiles(path, "*.json", SearchOption.AllDirectories))
        {
            var config = JsonConvert.DeserializeObject<ServicesRootConfiguration>(File.ReadAllText(file));
            allConfigs.Add(config);
        }

        // do your logic to "merge" the each config into a single ServicesConfiguration
        // here I simply select the AssemblyName from all files.
        var mergedConfig = new ServicesConfiguration
        {
            Services = allConfigs.SelectMany(c => c.ServicesConfiguration.Services).ToList()
        };

        return mergedConfig;
    }
}

然后在您的Controller中,通过DI正常获取实例。

public class HomeController : Controller
{
    private readonly ServicesConfiguration _config;

    public HomeController(ServicesConfiguration config)
    {
        _config = config ?? throw new ArgumentNullException(nameof(config));
    }
}

使用这种方法,您最终得到与通常注册IOptions相同的行为。但是,您避免了对它的依赖,并且不必使用丑陋的.Value(呕)。更好的是,您可以将其注册为接口,这在测试/模拟期间会让您的生活更加轻松。


1

我在尝试合并来自多个appsettings的数组时,在dotnet 6中遇到了类似的问题,后来偶然发现了这个线程。
解决方案实际上比我想象的要简单得多。
Microsoft.Extensions.Configuration通过索引合并数组:
{ foo: [1, 2, 3] } + { foo: [4, 5] } = { foo: 4, 5, 3 }

但我们希望能够声明哪些条目会覆盖其他条目,哪些条目应添加到列表中。我们通过将GUID声明为字典键而不是数组来实现这一点。

{
  "foo": {
     "870622cb-0372-49f3-a46e-07a1bd0db769": 1,
     "cbb3af55-94ea-41a5-bbb5-cb936ac47249": 2,
     "9410fcdc-28b3-4bff-bfed-4d7286b33294": 3
  }
}
+
{
  "foo": {
    "cbb3af55-94ea-41a5-bbb5-cb936ac47249": 4,
    "1c43fa78-b8db-41f8-809d-759a4bc35ee2": 5,
  }
}
=
{
  "foo": {
    "870622cb-0372-49f3-a46e-07a1bd0db769": 1,
    "cbb3af55-94ea-41a5-bbb5-cb936ac47249": 4, /*2 changed to 4 because key equals*/
    "9410fcdc-28b3-4bff-bfed-4d7286b33294": 3
    "1c43fa78-b8db-41f8-809d-759a4bc35ee2": 5, /*while 5 added to the list*/
  }
}

这可能一开始看起来有点不方便,因为人们会认为以下代码会抛出某种无效类型异常: ((IConfiguration)config).GetSection("foo").Get<int[]>(); 由于 Guid 不是我们所知道的数组索引,但它实际上可以(隐式地!)将上面合并的 "foo" 部分作为 int[] 返回。

1
这种方法非常有效。 - undefined
这个功能也适用于从列表中删除元素吗? - undefined
而且这个密钥不需要是全局唯一标识符(GUID),对吗? - undefined
1
@GoodNightNerdPride 不是的。它可以是你想要的任何东西。 :) - undefined
关于列表的问题...好问题。我可能会尝试使用set null的方法,并结合linq进行过滤... - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接