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

ThreeMammals / Ocelot / 28789930379

06 Jul 2026 12:00PM UTC coverage: 96.992% (-0.01%) from 97.004%
28789930379

Pull #1183

github

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

6611 of 6816 relevant lines covered (96.99%)

2347.43 hits per line

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

97.94
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
    /// <summary>
21
    /// Adds Ocelot configuration by environment, reading the required files from the default path.
22
    /// </summary>
23
    /// <param name="builder">Configuration builder to extend.</param>
24
    /// <param name="env">Web hosting environment object.</param>
25
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
26
    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env)
27
        => builder.AddOcelot(".", env);
2✔
28

29
    /// <summary>
30
    /// Adds Ocelot configuration by environment, reading the required files from the specified folder.
31
    /// </summary>
32
    /// <param name="builder">Configuration builder to extend.</param>
33
    /// <param name="folder">Folder to read files from.</param>
34
    /// <param name="env">Web hosting environment object.</param>
35
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
36
    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env)
37
        => builder.AddOcelot(folder, env, MergeOcelotJson.ToFile);
2✔
38

39
    /// <summary>
40
    /// Adds Ocelot configuration by environment and merge option, reading the required files from the current default folder.
41
    /// </summary>
42
    /// <remarks>Use optional arguments for injections and overridings.</remarks>
43
    /// <param name="builder">Configuration builder to extend.</param>
44
    /// <param name="env">Web hosting environment object.</param>
45
    /// <param name="mergeTo">Option to merge files to.</param>
46
    /// <param name="primaryConfigFile">Primary config file.</param>
47
    /// <param name="globalConfigFile">Global config file.</param>
48
    /// <param name="environmentConfigFile">Environment config file.</param>
49
    /// <param name="optional">The 2nd argument of the AddJsonFile.</param>
50
    /// <param name="reloadOnChange">The 3rd argument of the AddJsonFile.</param>
51
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
52
    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env, MergeOcelotJson mergeTo,
53
        string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections
54
        => builder.AddOcelot(".", env, mergeTo, primaryConfigFile, globalConfigFile, environmentConfigFile, optional, reloadOnChange);
3✔
55

56
    /// <summary>
57
    /// Adds Ocelot configuration by environment and merge option, reading the required files from the specified folder.
58
    /// </summary>
59
    /// <remarks>Use optional arguments for injections and overridings.</remarks>
60
    /// <param name="builder">Configuration builder to extend.</param>
61
    /// <param name="folder">Folder to read files from.</param>
62
    /// <param name="env">Web hosting environment object.</param>
63
    /// <param name="mergeTo">Option to merge files to.</param>
64
    /// <param name="primaryConfigFile">Primary config file.</param>
65
    /// <param name="globalConfigFile">Global config file.</param>
66
    /// <param name="environmentConfigFile">Environment config file.</param>
67
    /// <param name="optional">The 2nd argument of the AddJsonFile.</param>
68
    /// <param name="reloadOnChange">The 3rd argument of the AddJsonFile.</param>
69
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
70
    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env, MergeOcelotJson mergeTo,
71
        string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections
72
    {
11✔
73
        var json = GetMergedOcelotJson(folder, env, null, primaryConfigFile, globalConfigFile, environmentConfigFile);
11✔
74
        primaryConfigFile ??= Path.Join(folder, PrimaryConfigFile); // if not specified, merge & write back to the same folder
11✔
75
        return ApplyMergeOcelotJsonOption(builder, mergeTo, json, primaryConfigFile, optional, reloadOnChange);
11✔
76
    }
11✔
77

78
    private static IConfigurationBuilder ApplyMergeOcelotJsonOption(IConfigurationBuilder builder, MergeOcelotJson mergeTo, string json,
79
        string primaryConfigFile, bool? optional, bool? reloadOnChange)
80
    {
17✔
81
        return mergeTo == MergeOcelotJson.ToMemory ? 
17✔
82
            builder.AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) : 
17✔
83
            AddOcelotJsonFile(builder, json, primaryConfigFile, optional, reloadOnChange);
17✔
84
    }
17✔
85

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

89
    /// <summary>
90
    /// Merges multiple Ocelot configuration files (primary, global, environment-specific, and additional sub-configs) into a single JSON string.
91
    /// </summary>
92
    /// <remarks>
93
    /// <para>
94
    /// This is the core merging method used by all <c>AddOcelot</c> overloads. It discovers files matching the pattern 
95
    /// <c>ocelot.*.json</c> (case-insensitive), excluding the environment-specific file, and merges them according to 
96
    /// Ocelot's configuration merging rules (via <see cref="OcelotMergeConfiguration"/>).
97
    /// </para>
98
    /// <para>Supports splitting configuration across files for better maintainability (e.g., routes in separate files, global settings, environment overrides).</para>
99
    /// <para>Regex cache size is optimized for performance (see <see cref="RegexGlobal.RegexCacheSize"/>).</para>
100
    /// </remarks>
101
    /// <param name="folder">The root folder where configuration files are located.</param>
102
    /// <param name="env">The web hosting environment used to determine the environment-specific config file name.</param>
103
    /// <param name="fileConfiguration">Optional base <see cref="FileConfiguration"/> to start merging from.
104
    /// If <see langword="null"/>, a new empty instance is used.</param>
105
    /// <param name="primaryFile">Optional full path to the primary configuration file (defaults to <c>ocelot.json</c> in the given folder).
106
    /// Used to skip re-processing the primary file when multiple files exist.</param>
107
    /// <param name="globalFile">Optional full path to the global configuration file (defaults to <c>ocelot.global.json</c> in the given folder).</param>
108
    /// <param name="environmentFile">Optional full path to the environment-specific configuration file (defaults to <c>ocelot.{EnvironmentName}.json</c>).</param>
109
    /// <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>
110
    public static string GetMergedOcelotJson(string folder, IWebHostEnvironment env,
111
        FileConfiguration fileConfiguration = null, string primaryFile = null, string globalFile = null, string environmentFile = null)
112
    {
18✔
113
        // All versions of overloaded AddOcelot methods call this GetMergedOcelotJson one, so we improve Regex performance by cache increasing.
114
        // Developers can adjust the RegexGlobal value BEFORE calling AddOcelot
115
        // Developers can adjust the Regex.CacheSize value AFTER calling AddOcelot
116
        Regex.CacheSize = RegexGlobal.RegexCacheSize;
18✔
117

118
        var envName = string.IsNullOrEmpty(env?.EnvironmentName) ? "Development" : env.EnvironmentName;
18✔
119
        environmentFile ??= Path.Join(folder, string.Format(EnvironmentConfigFile, envName));
18✔
120
        var reg = SubConfigRegex();
18✔
121
        var environmentFileInfo = new FileInfo(environmentFile);
18✔
122
        var files = new DirectoryInfo(folder)
18✔
123
            .EnumerateFiles()
18✔
124
            .Where(fi => reg.IsMatch(fi.Name) &&
18✔
125
                !fi.Name.Equals(environmentFileInfo.Name, StringComparison.OrdinalIgnoreCase) &&
18✔
126
                !fi.FullName.Equals(environmentFileInfo.FullName, StringComparison.OrdinalIgnoreCase))
18✔
127
            .ToArray();
18✔
128

129
        fileConfiguration ??= new FileConfiguration();
18✔
130
        dynamic fcMerged = JObject.FromObject(fileConfiguration);
18✔
131
        fcMerged.GlobalConfiguration ??= new JObject();
18✔
132
        fcMerged.Aggregates ??= new JArray();
18✔
133
        fcMerged.Routes ??= new JArray();
18✔
134

135
        primaryFile ??= Path.Join(folder, PrimaryConfigFile);
18✔
136
        globalFile ??= Path.Join(folder, GlobalConfigFile);
18✔
137
        var primaryFileInfo = new FileInfo(primaryFile);
18✔
138
        var globalFileInfo = new FileInfo(globalFile);
18✔
139
        foreach (var file in files)
180✔
140
        {
63✔
141
            if (files.Length > 1 &&
63✔
142
                file.Name.Equals(primaryFileInfo.Name, StringComparison.OrdinalIgnoreCase) &&
63✔
143
                file.FullName.Equals(primaryFileInfo.FullName, StringComparison.OrdinalIgnoreCase))
63✔
144
            {
1✔
145
                continue;
1✔
146
            }
147

148
            var lines = File.ReadAllText(file.FullName);
62✔
149
            dynamic fromConfig = JToken.Parse(lines);
62✔
150
            bool isGlobal = file.Name.Equals(globalFileInfo.Name, StringComparison.OrdinalIgnoreCase) &&
62✔
151
                file.FullName.Equals(globalFileInfo.FullName, StringComparison.OrdinalIgnoreCase);
62✔
152
            OcelotMergeConfiguration(fcMerged, fromConfig, isGlobal);
62✔
153
        }
62✔
154

155
        return ((JObject)fcMerged).ToString();
18✔
156
    }
18✔
157

158
    /// <summary>
159
    /// Adds Ocelot configuration from a ready Newtonsoft's <see cref="JObject"/> and writes the serialized JSON to the primary configuration file.
160
    /// </summary>
161
    /// <remarks>
162
    /// The JSON is serialized with indentation and written to the primary config file (default: <see cref="PrimaryConfigFile"/> = <c>ocelot.json</c>).<br/>
163
    /// Finally, the file is added as a JSON configuration provider via <see cref="JsonConfigurationExtensions.AddJsonFile(IConfigurationBuilder, string, bool, bool)"/>.<br/>
164
    /// Use optional arguments for custom file paths and <c>AddJsonFile</c> behavior.
165
    /// </remarks>
166
    /// <param name="builder">Configuration builder to extend.</param>
167
    /// <param name="fileConfiguration">The Ocelot configuration as a <see cref="JObject"/>.</param>
168
    /// <param name="primaryConfigFile">Primary config file path. If <see langword="null"/>, defaults to <see cref="PrimaryConfigFile"/>.</param>
169
    /// <param name="optional">The optional parameter for <see cref="JsonConfigurationExtensions.AddJsonFile(IConfigurationBuilder, string, bool, bool)"/>.</param>
170
    /// <param name="reloadOnChange">The reloadOnChange parameter for <see cref="JsonConfigurationExtensions.AddJsonFile(IConfigurationBuilder, string, bool, bool)"/>.</param>
171
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
172
    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, JObject fileConfiguration,
173
        string primaryConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections
174
        => SerializeToFile(builder, fileConfiguration, primaryConfigFile, optional, reloadOnChange);
×
175

176
    /// <summary>
177
    /// Adds Ocelot configuration by ready configuration object and writes JSON to the primary configuration file.<br/>
178
    /// Finally, adds JSON file as configuration provider.
179
    /// </summary>
180
    /// <remarks>Use optional arguments for injections and overridings.</remarks>
181
    /// <param name="builder">Configuration builder to extend.</param>
182
    /// <param name="fileConfiguration">File configuration to add as JSON provider.</param>
183
    /// <param name="primaryConfigFile">Primary config file.</param>
184
    /// <param name="optional">The 2nd argument of the AddJsonFile.</param>
185
    /// <param name="reloadOnChange">The 3rd argument of the AddJsonFile.</param>
186
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
187
    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration,
188
        string primaryConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections
189
        => SerializeToFile(builder, fileConfiguration, primaryConfigFile, optional, reloadOnChange);
1✔
190

191
    private static IConfigurationBuilder SerializeToFile(IConfigurationBuilder builder, object fileConfiguration,
192
        string primaryConfigFile = null, bool? optional = null, bool? reloadOnChange = null)
193
    {
1✔
194
        var json = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented);
1✔
195
        return AddOcelotJsonFile(builder, json, primaryConfigFile, optional, reloadOnChange);
1✔
196
    }
1✔
197

198
    /// <summary>
199
    /// Adds Ocelot configuration by ready configuration object, environment and merge option, reading the required files from the current default folder.
200
    /// </summary>
201
    /// <param name="builder">Configuration builder to extend.</param>
202
    /// <param name="fileConfiguration">File configuration to add as JSON provider.</param>
203
    /// <param name="env">Web hosting environment object.</param>
204
    /// <param name="mergeTo">Option to merge files to.</param>
205
    /// <param name="primaryConfigFile">Primary config file.</param>
206
    /// <param name="globalConfigFile">Global config file.</param>
207
    /// <param name="environmentConfigFile">Environment config file.</param>
208
    /// <param name="optional">The 2nd argument of the AddJsonFile.</param>
209
    /// <param name="reloadOnChange">The 3rd argument of the AddJsonFile.</param>
210
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
211
    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, IWebHostEnvironment env, MergeOcelotJson mergeTo,
212
        string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections
213
    {
6✔
214
        var json = GetMergedOcelotJson(".", env, fileConfiguration, primaryConfigFile, globalConfigFile, environmentConfigFile);
6✔
215
        return ApplyMergeOcelotJsonOption(builder, mergeTo, json, primaryConfigFile, optional, reloadOnChange);
6✔
216
    }
6✔
217

218
    /// <summary>
219
    /// Adds Ocelot primary configuration file (aka ocelot.json).<br/>
220
    /// Writes JSON to the file.<br/>
221
    /// Adds the file as a JSON configuration provider via the <see cref="JsonConfigurationExtensions.AddJsonFile(IConfigurationBuilder, string, bool, bool)"/> extension.
222
    /// </summary>
223
    /// <remarks>Use optional arguments for injections and overridings.</remarks>
224
    /// <param name="builder">The builder to extend.</param>
225
    /// <param name="json">JSON data of the Ocelot configuration.</param>
226
    /// <param name="primaryFile">Primary config file.</param>
227
    /// <param name="optional">The 2nd argument of the AddJsonFile.</param>
228
    /// <param name="reloadOnChange">The 3rd argument of the AddJsonFile.</param>
229
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
230
    public static IConfigurationBuilder AddOcelotJsonFile(this IConfigurationBuilder builder, string json,
231
        string primaryFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections
232
    {
14✔
233
        var primary = primaryFile ?? PrimaryConfigFile;
14✔
234
        File.WriteAllText(primary, json);
14✔
235
        return builder.AddJsonFile(primary, optional ?? false, reloadOnChange ?? false);
14✔
236
    }
14✔
237

238
    /// <summary>
239
    /// Adds Ocelot primary configuration file (aka ocelot.json) in read-only mode.
240
    /// <para>Adds the file as a JSON configuration provider via the <see cref="JsonConfigurationExtensions.AddJsonFile(IConfigurationBuilder, string, bool, bool)"/> extension.</para>
241
    /// </summary>
242
    /// <remarks>Use optional arguments for injections and overridings.</remarks>
243
    /// <param name="builder">The builder to extend.</param>
244
    /// <param name="primaryFile">Primary config file path.</param>
245
    /// <param name="optional">The 2nd argument of the AddJsonFile.</param>
246
    /// <param name="reloadOnChange">The 3rd argument of the AddJsonFile.</param>
247
    /// <returns>An <see cref="IConfigurationBuilder"/> object.</returns>
248
    public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder,
249
        string primaryFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections
250
        => builder.AddJsonFile(primaryFile ?? PrimaryConfigFile, optional ?? false, reloadOnChange ?? false);
8✔
251

252
    /// <summary>
253
    /// Merges configuration sections from one Newtonsoft's <see cref="JToken"/> into another according to Ocelot's merging rules.
254
    /// </summary>
255
    /// <remarks>
256
    /// If <paramref name="isGlobal"/> is <see langword="true"/>, the <see cref="FileConfiguration.GlobalConfiguration"/> section is merged first.<br/>
257
    /// Then <c>Aggregates</c>, <c>Routes</c>, and <c>DynamicRoutes</c> sections are merged (using array merge for collections).<br/>
258
    /// This is an internal helper used by <see cref="GetMergedOcelotJson"/>.
259
    /// </remarks>
260
    /// <param name="to">The target <see cref="JToken"/> (usually the merged configuration).</param>
261
    /// <param name="from">The source <see cref="JToken"/> to merge from.</param>
262
    /// <param name="isGlobal">Indicates whether the source contains global configuration that should be merged into <c>GlobalConfiguration</c> section.</param>
263
    public static void OcelotMergeConfiguration(this JToken to, JToken from, bool isGlobal)
264
    {
62✔
265
        if (isGlobal)
62✔
266
            to.OcelotMergeConfigurationSection(from, nameof(FileConfiguration.GlobalConfiguration));
10✔
267

268
        to.OcelotMergeConfigurationSection(from, nameof(FileConfiguration.Aggregates))
62✔
269
          .OcelotMergeConfigurationSection(from, nameof(FileConfiguration.Routes))
62✔
270
          .OcelotMergeConfigurationSection(from, nameof(FileConfiguration.DynamicRoutes));
62✔
271
    }
62✔
272

273
    /// <summary>Merges a single named section from source to target Newtonsoft's <see cref="JToken"/>.</summary>
274
    /// <remarks>
275
    /// For <see cref="JObject"/> sections (e.g. <c>GlobalConfiguration</c>), the source completely replaces the destination.<br/>
276
    /// For <see cref="JArray"/> sections (e.g. <c>Routes</c>, <c>Aggregates</c>), items are merged using <see cref="JArray.MergeItem(object, JsonMergeSettings?)"/>.
277
    /// </remarks>
278
    /// <param name="to">The target <see cref="JToken"/>.</param>
279
    /// <param name="from">The source <see cref="JToken"/>.</param>
280
    /// <param name="sectionName">Name of the section to merge (case-insensitive).</param>
281
    /// <returns>The original target <see cref="JToken"/> to allow method chaining.</returns>
282
    public static JToken OcelotMergeConfigurationSection(this JToken to, JToken from, string sectionName)
283
    {
196✔
284
        var destination = to.OcelotGetSection(sectionName);
196✔
285
        var source = from.OcelotGetSection(sectionName);
196✔
286
        if (source == null || destination == null)
196✔
287
            return to;
66✔
288

289
        if (source is JObject)
130✔
290
            to.OcelotSetSection(sectionName, source);
10✔
291
        else if (source is JArray)
120✔
292
            (destination as JArray).Merge(source);
120✔
293

294
        return to;
130✔
295
    }
196✔
296

297
    /// <summary>Retrieves a named section from a Newtonsoft's <see cref="JToken"/> (case-insensitive lookup).</summary>
298
    /// <param name="token">The <see cref="JToken"/> (typically a <see cref="JObject"/> containing Ocelot configuration).</param>
299
    /// <param name="name">The name of the section to retrieve (e.g. "Routes", "GlobalConfiguration").</param>
300
    /// <returns>The section as <see cref="JToken"/> if found; otherwise <see langword="null"/>.</returns>
301
    public static JToken OcelotGetSection(this JToken token, string name)
302
    {
392✔
303
        var obj = token as JObject;
392✔
304
        return obj?.Properties()
392✔
305
            .FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
392✔
306
            ?.Value;
392✔
307
    }
392✔
308

309
    /// <summary>Sets (or adds) a named section in the target Newtonsoft's <see cref="JToken"/>.</summary>
310
    /// <remarks>Performs a case-insensitive lookup for an existing property before updating or adding it.</remarks>
311
    /// <param name="to">The target <see cref="JToken"/> (must be a <see cref="JObject"/>).</param>
312
    /// <param name="name">The name of the section/property to set.</param>
313
    /// <param name="value">The value to assign to the section.</param>
314
    public static void OcelotSetSection(this JToken to, string name, JToken value)
315
    {
10✔
316
        JObject obj = to as JObject;
10✔
317
        var prop = obj?.Properties()
10✔
318
            .FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
10✔
319
        if (prop != null)
10✔
320
            prop.Value = value;
10✔
321
        else
322
            obj.Add(name, value);
×
323
    }
10✔
324
}
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