• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ThreeMammals / Ocelot / 28741082813

05 Jul 2026 12:40PM UTC coverage: 96.936% (-0.07%) from 97.004%
28741082813

Pull #1183

github

web-flow
Merge eedf90dcf into c5ee51014
Pull Request #1183: #651 Merge custom JSON properties across multiple `ocelot.X.json` configuration files using Newtonsoft's `JToken` merge functionality

6613 of 6822 relevant lines covered (96.94%)

2132.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.13
src/DependencyInjection/ConfigurationBuilderExtensions.cs
1
using Microsoft.AspNetCore.Hosting;
2
using Microsoft.Extensions.Configuration;
3
using Microsoft.Extensions.Configuration.Memory;
4
using Newtonsoft.Json;
5
using Newtonsoft.Json.Linq;
6
using Ocelot.Configuration.File;
7
using Ocelot.Infrastructure;
8

9
namespace Ocelot.DependencyInjection;
10

11
/// <summary>
12
/// Defines extension-methods for the <see cref="IConfigurationBuilder"/> interface.
13
/// </summary>
14
public static partial class ConfigurationBuilderExtensions
15
{
16
    public const string PrimaryConfigFile = "ocelot.json";
17
    public const string GlobalConfigFile = "ocelot.global.json";
18
    public const string EnvironmentConfigFile = "ocelot.{0}.json";
19

20
    [Obsolete("Please set BaseUrl in ocelot.json GlobalConfiguration.BaseUrl")]
21
    public static IConfigurationBuilder AddOcelotBaseUrl(this IConfigurationBuilder builder, string baseUrl)
22
    {
1✔
23
        var memorySource = new MemoryConfigurationSource
1✔
24
        {
1✔
25
            InitialData = new List<KeyValuePair<string, string>>
1✔
26
            {
1✔
27
                new("BaseUrl", baseUrl),
1✔
28
            },
1✔
29
        };
1✔
30

31
        return builder.Add(memorySource);
1✔
32
    }
1✔
33

34
    /// <summary>
35
    /// Adds Ocelot configuration by environment, reading the required files from the default path.
36
    /// </summary>
37
    /// <param name="builder">Configuration builder to extend.</param>
38
    /// <param name="env">Web hosting environment object.</param>
39
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
40
    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env)
41
        => builder.AddOcelot(".", env);
2✔
42

43
    /// <summary>
44
    /// Adds Ocelot configuration by environment, reading the required files from the specified folder.
45
    /// </summary>
46
    /// <param name="builder">Configuration builder to extend.</param>
47
    /// <param name="folder">Folder to read files from.</param>
48
    /// <param name="env">Web hosting environment object.</param>
49
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
50
    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env)
51
        => builder.AddOcelot(folder, env, MergeOcelotJson.ToFile);
2✔
52

53
    /// <summary>
54
    /// Adds Ocelot configuration by environment and merge option, reading the required files from the current default folder.
55
    /// </summary>
56
    /// <remarks>Use optional arguments for injections and overridings.</remarks>
57
    /// <param name="builder">Configuration builder to extend.</param>
58
    /// <param name="env">Web hosting environment object.</param>
59
    /// <param name="mergeTo">Option to merge files to.</param>
60
    /// <param name="primaryConfigFile">Primary config file.</param>
61
    /// <param name="globalConfigFile">Global config file.</param>
62
    /// <param name="environmentConfigFile">Environment config file.</param>
63
    /// <param name="optional">The 2nd argument of the AddJsonFile.</param>
64
    /// <param name="reloadOnChange">The 3rd argument of the AddJsonFile.</param>
65
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
66
    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env, MergeOcelotJson mergeTo,
67
        string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections
68
        => builder.AddOcelot(".", env, mergeTo, primaryConfigFile, globalConfigFile, environmentConfigFile, optional, reloadOnChange);
3✔
69

70
    /// <summary>
71
    /// Adds Ocelot configuration by environment and merge option, reading the required files from the specified folder.
72
    /// </summary>
73
    /// <remarks>Use optional arguments for injections and overridings.</remarks>
74
    /// <param name="builder">Configuration builder to extend.</param>
75
    /// <param name="folder">Folder to read files from.</param>
76
    /// <param name="env">Web hosting environment object.</param>
77
    /// <param name="mergeTo">Option to merge files to.</param>
78
    /// <param name="primaryConfigFile">Primary config file.</param>
79
    /// <param name="globalConfigFile">Global config file.</param>
80
    /// <param name="environmentConfigFile">Environment config file.</param>
81
    /// <param name="optional">The 2nd argument of the AddJsonFile.</param>
82
    /// <param name="reloadOnChange">The 3rd argument of the AddJsonFile.</param>
83
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
84
    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env, MergeOcelotJson mergeTo,
85
        string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections
86
    {
11✔
87
        var json = GetMergedOcelotJson(folder, env, null, primaryConfigFile, globalConfigFile, environmentConfigFile);
11✔
88
        primaryConfigFile ??= Path.Join(folder, PrimaryConfigFile); // if not specified, merge & write back to the same folder
11✔
89
        return ApplyMergeOcelotJsonOption(builder, mergeTo, json, primaryConfigFile, optional, reloadOnChange);
11✔
90
    }
11✔
91

92
    private static IConfigurationBuilder ApplyMergeOcelotJsonOption(IConfigurationBuilder builder, MergeOcelotJson mergeTo, string json,
93
        string primaryConfigFile, bool? optional, bool? reloadOnChange)
94
    {
17✔
95
        return mergeTo == MergeOcelotJson.ToMemory ? 
17✔
96
            builder.AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) : 
17✔
97
            AddOcelotJsonFile(builder, json, primaryConfigFile, optional, reloadOnChange);
17✔
98
    }
17✔
99

100
    [GeneratedRegex(@"^ocelot\.(.*?)\.json$", RegexOptions.IgnoreCase | RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds, "en-US")]
101
    private static partial Regex SubConfigRegex();
102

103
    /// <summary>
104
    /// Merges multiple Ocelot configuration files (primary, global, environment-specific, and additional sub-configs) into a single JSON string.
105
    /// </summary>
106
    /// <remarks>
107
    /// <para>
108
    /// This is the core merging method used by all <c>AddOcelot</c> overloads. It discovers files matching the pattern 
109
    /// <c>ocelot.*.json</c> (case-insensitive), excluding the environment-specific file, and merges them according to 
110
    /// Ocelot's configuration merging rules (via <see cref="OcelotMergeConfiguration"/>).
111
    /// </para>
112
    /// <para>Supports splitting configuration across files for better maintainability (e.g., routes in separate files, global settings, environment overrides).</para>
113
    /// <para>Regex cache size is optimized for performance (see <see cref="RegexGlobal.RegexCacheSize"/>).</para>
114
    /// </remarks>
115
    /// <param name="folder">The root folder where configuration files are located.</param>
116
    /// <param name="env">The web hosting environment used to determine the environment-specific config file name.</param>
117
    /// <param name="fileConfiguration">Optional base <see cref="FileConfiguration"/> to start merging from.
118
    /// If <see langword="null"/>, a new empty instance is used.</param>
119
    /// <param name="primaryFile">Optional full path to the primary configuration file (defaults to <c>ocelot.json</c> in the given folder).
120
    /// Used to skip re-processing the primary file when multiple files exist.</param>
121
    /// <param name="globalFile">Optional full path to the global configuration file (defaults to <c>ocelot.global.json</c> in the given folder).</param>
122
    /// <param name="environmentFile">Optional full path to the environment-specific configuration file (defaults to <c>ocelot.{EnvironmentName}.json</c>).</param>
123
    /// <returns>A JSON <see cref="string"/> containing the fully merged Ocelot configuration (ready to be written to file or added as a memory source).</returns>
124
    public static string GetMergedOcelotJson(string folder, IWebHostEnvironment env,
125
        FileConfiguration fileConfiguration = null, string primaryFile = null, string globalFile = null, string environmentFile = null)
126
    {
18✔
127
        // All versions of overloaded AddOcelot methods call this GetMergedOcelotJson one, so we improve Regex performance by cache increasing.
128
        // Developers can adjust the RegexGlobal value BEFORE calling AddOcelot
129
        // Developers can adjust the Regex.CacheSize value AFTER calling AddOcelot
130
        Regex.CacheSize = RegexGlobal.RegexCacheSize;
18✔
131

132
        var envName = string.IsNullOrEmpty(env?.EnvironmentName) ? "Development" : env.EnvironmentName;
18✔
133
        environmentFile ??= Path.Join(folder, string.Format(EnvironmentConfigFile, envName));
18✔
134
        var reg = SubConfigRegex();
18✔
135
        var environmentFileInfo = new FileInfo(environmentFile);
18✔
136
        var files = new DirectoryInfo(folder)
18✔
137
            .EnumerateFiles()
18✔
138
            .Where(fi => reg.IsMatch(fi.Name) &&
18✔
139
                !fi.Name.Equals(environmentFileInfo.Name, StringComparison.OrdinalIgnoreCase) &&
18✔
140
                !fi.FullName.Equals(environmentFileInfo.FullName, StringComparison.OrdinalIgnoreCase))
18✔
141
            .ToArray();
18✔
142

143
        fileConfiguration ??= new FileConfiguration();
18✔
144
        dynamic fcMerged = JObject.FromObject(fileConfiguration);
18✔
145
        fcMerged.GlobalConfiguration ??= new JObject();
18✔
146
        fcMerged.Aggregates ??= new JArray();
18✔
147
        fcMerged.Routes ??= new JArray();
18✔
148

149
        primaryFile ??= Path.Join(folder, PrimaryConfigFile);
18✔
150
        globalFile ??= Path.Join(folder, GlobalConfigFile);
18✔
151
        var primaryFileInfo = new FileInfo(primaryFile);
18✔
152
        var globalFileInfo = new FileInfo(globalFile);
18✔
153
        foreach (var file in files)
180✔
154
        {
63✔
155
            if (files.Length > 1 &&
63✔
156
                file.Name.Equals(primaryFileInfo.Name, StringComparison.OrdinalIgnoreCase) &&
63✔
157
                file.FullName.Equals(primaryFileInfo.FullName, StringComparison.OrdinalIgnoreCase))
63✔
158
            {
1✔
159
                continue;
1✔
160
            }
161

162
            var lines = File.ReadAllText(file.FullName);
62✔
163
            dynamic fromConfig = JToken.Parse(lines);
62✔
164
            bool isGlobal = file.Name.Equals(globalFileInfo.Name, StringComparison.OrdinalIgnoreCase) &&
62✔
165
                file.FullName.Equals(globalFileInfo.FullName, StringComparison.OrdinalIgnoreCase);
62✔
166
            OcelotMergeConfiguration(fcMerged, fromConfig, isGlobal);
62✔
167
        }
62✔
168

169
        return ((JObject)fcMerged).ToString();
18✔
170
    }
18✔
171

172
    /// <summary>
173
    /// Adds Ocelot configuration from a ready Newtonsoft's <see cref="JObject"/> and writes the serialized JSON to the primary configuration file.
174
    /// </summary>
175
    /// <remarks>
176
    /// The JSON is serialized with indentation and written to the primary config file (default: <see cref="PrimaryConfigFile"/> = <c>ocelot.json</c>).<br/>
177
    /// Finally, the file is added as a JSON configuration provider via <see cref="JsonConfigurationExtensions.AddJsonFile(IConfigurationBuilder, string, bool, bool)"/>.<br/>
178
    /// Use optional arguments for custom file paths and <c>AddJsonFile</c> behavior.
179
    /// </remarks>
180
    /// <param name="builder">Configuration builder to extend.</param>
181
    /// <param name="fileConfiguration">The Ocelot configuration as a <see cref="JObject"/>.</param>
182
    /// <param name="primaryConfigFile">Primary config file path. If <see langword="null"/>, defaults to <see cref="PrimaryConfigFile"/>.</param>
183
    /// <param name="optional">The optional parameter for <see cref="JsonConfigurationExtensions.AddJsonFile(IConfigurationBuilder, string, bool, bool)"/>.</param>
184
    /// <param name="reloadOnChange">The reloadOnChange parameter for <see cref="JsonConfigurationExtensions.AddJsonFile(IConfigurationBuilder, string, bool, bool)"/>.</param>
185
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
186
    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, JObject fileConfiguration,
187
        string primaryConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections
188
        => SerializeToFile(builder, fileConfiguration, primaryConfigFile, optional, reloadOnChange);
×
189

190
    /// <summary>
191
    /// Adds Ocelot configuration by ready configuration object and writes JSON to the primary configuration file.<br/>
192
    /// Finally, adds JSON file as configuration provider.
193
    /// </summary>
194
    /// <remarks>Use optional arguments for injections and overridings.</remarks>
195
    /// <param name="builder">Configuration builder to extend.</param>
196
    /// <param name="fileConfiguration">File configuration to add as JSON provider.</param>
197
    /// <param name="primaryConfigFile">Primary config file.</param>
198
    /// <param name="optional">The 2nd argument of the AddJsonFile.</param>
199
    /// <param name="reloadOnChange">The 3rd argument of the AddJsonFile.</param>
200
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
201
    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration,
202
        string primaryConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections
203
        => SerializeToFile(builder, fileConfiguration, primaryConfigFile, optional, reloadOnChange);
1✔
204

205
    private static IConfigurationBuilder SerializeToFile(IConfigurationBuilder builder, object fileConfiguration,
206
        string primaryConfigFile = null, bool? optional = null, bool? reloadOnChange = null)
207
    {
1✔
208
        var json = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented);
1✔
209
        return AddOcelotJsonFile(builder, json, primaryConfigFile, optional, reloadOnChange);
1✔
210
    }
1✔
211

212
    /// <summary>
213
    /// Adds Ocelot configuration by ready configuration object, environment and merge option, reading the required files from the current default folder.
214
    /// </summary>
215
    /// <param name="builder">Configuration builder to extend.</param>
216
    /// <param name="fileConfiguration">File configuration to add as JSON provider.</param>
217
    /// <param name="env">Web hosting environment object.</param>
218
    /// <param name="mergeTo">Option to merge files to.</param>
219
    /// <param name="primaryConfigFile">Primary config file.</param>
220
    /// <param name="globalConfigFile">Global config file.</param>
221
    /// <param name="environmentConfigFile">Environment config file.</param>
222
    /// <param name="optional">The 2nd argument of the AddJsonFile.</param>
223
    /// <param name="reloadOnChange">The 3rd argument of the AddJsonFile.</param>
224
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
225
    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, IWebHostEnvironment env, MergeOcelotJson mergeTo,
226
        string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections
227
    {
6✔
228
        var json = GetMergedOcelotJson(".", env, fileConfiguration, primaryConfigFile, globalConfigFile, environmentConfigFile);
6✔
229
        return ApplyMergeOcelotJsonOption(builder, mergeTo, json, primaryConfigFile, optional, reloadOnChange);
6✔
230
    }
6✔
231

232
    /// <summary>
233
    /// Adds Ocelot primary configuration file (aka ocelot.json).<br/>
234
    /// Writes JSON to the file.<br/>
235
    /// Adds the file as a JSON configuration provider via the <see cref="JsonConfigurationExtensions.AddJsonFile(IConfigurationBuilder, string, bool, bool)"/> extension.
236
    /// </summary>
237
    /// <remarks>Use optional arguments for injections and overridings.</remarks>
238
    /// <param name="builder">The builder to extend.</param>
239
    /// <param name="json">JSON data of the Ocelot configuration.</param>
240
    /// <param name="primaryFile">Primary config file.</param>
241
    /// <param name="optional">The 2nd argument of the AddJsonFile.</param>
242
    /// <param name="reloadOnChange">The 3rd argument of the AddJsonFile.</param>
243
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
244
    public static IConfigurationBuilder AddOcelotJsonFile(this IConfigurationBuilder builder, string json,
245
        string primaryFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections
246
    {
14✔
247
        var primary = primaryFile ?? PrimaryConfigFile;
14✔
248
        File.WriteAllText(primary, json);
14✔
249
        return builder.AddJsonFile(primary, optional ?? false, reloadOnChange ?? false);
14✔
250
    }
14✔
251

252
    /// <summary>
253
    /// Adds Ocelot primary configuration file (aka ocelot.json) in read-only mode.
254
    /// <para>Adds the file as a JSON configuration provider via the <see cref="JsonConfigurationExtensions.AddJsonFile(IConfigurationBuilder, string, bool, bool)"/> extension.</para>
255
    /// </summary>
256
    /// <remarks>Use optional arguments for injections and overridings.</remarks>
257
    /// <param name="builder">The builder to extend.</param>
258
    /// <param name="primaryFile">Primary config file path.</param>
259
    /// <param name="optional">The 2nd argument of the AddJsonFile.</param>
260
    /// <param name="reloadOnChange">The 3rd argument of the AddJsonFile.</param>
261
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
262
    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder,
263
        string primaryFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections
264
        => builder.AddJsonFile(primaryFile ?? PrimaryConfigFile, optional ?? false, reloadOnChange ?? false);
8✔
265

266
    /// <summary>
267
    /// Merges configuration sections from one Newtonsoft's <see cref="JToken"/> into another according to Ocelot's merging rules.
268
    /// </summary>
269
    /// <remarks>
270
    /// If <paramref name="isGlobal"/> is <see langword="true"/>, the <see cref="FileConfiguration.GlobalConfiguration"/> section is merged first.<br/>
271
    /// Then <c>Aggregates</c>, <c>Routes</c>, and <c>DynamicRoutes</c> sections are merged (using array merge for collections).<br/>
272
    /// This is an internal helper used by <see cref="GetMergedOcelotJson"/>.
273
    /// </remarks>
274
    /// <param name="to">The target <see cref="JToken"/> (usually the merged configuration).</param>
275
    /// <param name="from">The source <see cref="JToken"/> to merge from.</param>
276
    /// <param name="isGlobal">Indicates whether the source contains global configuration that should be merged into <c>GlobalConfiguration</c> section.</param>
277
    public static void OcelotMergeConfiguration(this JToken to, JToken from, bool isGlobal)
278
    {
62✔
279
        if (isGlobal)
62✔
280
            to.OcelotMergeConfigurationSection(from, nameof(FileConfiguration.GlobalConfiguration));
10✔
281

282
        to.OcelotMergeConfigurationSection(from, nameof(FileConfiguration.Aggregates))
62✔
283
          .OcelotMergeConfigurationSection(from, nameof(FileConfiguration.Routes))
62✔
284
          .OcelotMergeConfigurationSection(from, nameof(FileConfiguration.DynamicRoutes));
62✔
285
    }
62✔
286

287
    /// <summary>Merges a single named section from source to target Newtonsoft's <see cref="JToken"/>.</summary>
288
    /// <remarks>
289
    /// For <see cref="JObject"/> sections (e.g. <c>GlobalConfiguration</c>), the source completely replaces the destination.<br/>
290
    /// For <see cref="JArray"/> sections (e.g. <c>Routes</c>, <c>Aggregates</c>), items are merged using <see cref="JArray.MergeItem(object, JsonMergeSettings?)"/>.
291
    /// </remarks>
292
    /// <param name="to">The target <see cref="JToken"/>.</param>
293
    /// <param name="from">The source <see cref="JToken"/>.</param>
294
    /// <param name="sectionName">Name of the section to merge (case-insensitive).</param>
295
    /// <returns>The original target <see cref="JToken"/> to allow method chaining.</returns>
296
    public static JToken OcelotMergeConfigurationSection(this JToken to, JToken from, string sectionName)
297
    {
196✔
298
        var destination = to.OcelotGetSection(sectionName);
196✔
299
        var source = from.OcelotGetSection(sectionName);
196✔
300
        if (source == null || destination == null)
196✔
301
            return to;
66✔
302

303
        if (source is JObject)
130✔
304
            to.OcelotSetSection(sectionName, source);
10✔
305
        else if (source is JArray)
120✔
306
            (destination as JArray).Merge(source);
120✔
307

308
        return to;
130✔
309
    }
196✔
310

311
    /// <summary>Retrieves a named section from a Newtonsoft's <see cref="JToken"/> (case-insensitive lookup).</summary>
312
    /// <param name="token">The <see cref="JToken"/> (typically a <see cref="JObject"/> containing Ocelot configuration).</param>
313
    /// <param name="name">The name of the section to retrieve (e.g. "Routes", "GlobalConfiguration").</param>
314
    /// <returns>The section as <see cref="JToken"/> if found; otherwise <see langword="null"/>.</returns>
315
    public static JToken OcelotGetSection(this JToken token, string name)
316
    {
392✔
317
        var obj = token as JObject;
392✔
318
        return obj?.Properties()
392✔
319
            .FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
392✔
320
            ?.Value;
392✔
321
    }
392✔
322

323
    /// <summary>Sets (or adds) a named section in the target Newtonsoft's <see cref="JToken"/>.</summary>
324
    /// <remarks>Performs a case-insensitive lookup for an existing property before updating or adding it.</remarks>
325
    /// <param name="to">The target <see cref="JToken"/> (must be a <see cref="JObject"/>).</param>
326
    /// <param name="name">The name of the section/property to set.</param>
327
    /// <param name="value">The value to assign to the section.</param>
328
    public static void OcelotSetSection(this JToken to, string name, JToken value)
329
    {
10✔
330
        JObject obj = to as JObject;
10✔
331
        var prop = obj?.Properties()
10✔
332
            .FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
10✔
333
        if (prop != null)
10✔
334
            prop.Value = value;
10✔
335
        else
336
            obj.Add(name, value);
×
337
    }
10✔
338
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc