diff --git a/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationExtensions.cs b/src/OrchardCore/OrchardCore.Abstractions/Modules/Overrides/Configuration/TenantJsonConfigurationExtensions.cs
similarity index 87%
rename from src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationExtensions.cs
rename to src/OrchardCore/OrchardCore.Abstractions/Modules/Overrides/Configuration/TenantJsonConfigurationExtensions.cs
index ba63c908549..63bc9c0d058 100644
--- a/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationExtensions.cs
+++ b/src/OrchardCore/OrchardCore.Abstractions/Modules/Overrides/Configuration/TenantJsonConfigurationExtensions.cs
@@ -93,5 +93,19 @@ public static IConfigurationBuilder AddTenantJsonFile(this IConfigurationBuilder
/// The .
public static IConfigurationBuilder AddTenantJsonFile(this IConfigurationBuilder builder, Action? configureSource)
=> builder.Add(configureSource);
+
+ ///
+ /// Adds a JSON configuration source to .
+ ///
+ /// The to add to.
+ /// The to read the json configuration data from.
+ /// The .
+ public static IConfigurationBuilder AddTenantJsonStream(this IConfigurationBuilder builder, Stream stream)
+ {
+ // ThrowHelper.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(builder);
+
+ return builder.Add(s => s.Stream = stream);
+ }
}
}
diff --git a/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationProvider.cs b/src/OrchardCore/OrchardCore.Abstractions/Modules/Overrides/Configuration/TenantJsonConfigurationProvider.cs
similarity index 66%
rename from src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationProvider.cs
rename to src/OrchardCore/OrchardCore.Abstractions/Modules/Overrides/Configuration/TenantJsonConfigurationProvider.cs
index 427f05b29da..6c578edc7f2 100644
--- a/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationProvider.cs
+++ b/src/OrchardCore/OrchardCore.Abstractions/Modules/Overrides/Configuration/TenantJsonConfigurationProvider.cs
@@ -1,6 +1,6 @@
using System;
using System.IO;
-using System.Text.Json;
+using OrchardCore.Environment.Shell.Configuration.Internal;
namespace Microsoft.Extensions.Configuration.Json
{
@@ -19,18 +19,7 @@ public TenantJsonConfigurationProvider(TenantJsonConfigurationSource source) : b
/// Loads the JSON data from a stream.
///
/// The stream to read.
- public override void Load(Stream stream)
- {
- try
- {
- Data = JsonConfigurationFileParser.Parse(stream);
- }
- catch (JsonException e)
- {
- // throw new FormatException(SR.Error_JSONParseError, e);
- throw new FormatException("Could not parse the JSON file.", e);
- }
- }
+ public override void Load(Stream stream) => Data = JsonConfigurationParser.Parse(stream);
///
/// Dispose the provider.
@@ -40,8 +29,11 @@ protected override void Dispose(bool disposing)
{
base.Dispose(true);
- // OC: Will be part of 'FileConfigurationProvider'.
- (Source.FileProvider as IDisposable)?.Dispose();
+ // OC: Will be part of 'FileConfigurationProvider' in a future version.
+ // if (Source.OwnsFileProvider)
+ {
+ (Source.FileProvider as IDisposable)?.Dispose();
+ }
}
}
}
diff --git a/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationSource.cs b/src/OrchardCore/OrchardCore.Abstractions/Modules/Overrides/Configuration/TenantJsonConfigurationSource.cs
similarity index 100%
rename from src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/TenantJsonConfigurationSource.cs
rename to src/OrchardCore/OrchardCore.Abstractions/Modules/Overrides/Configuration/TenantJsonConfigurationSource.cs
diff --git a/src/OrchardCore/OrchardCore.Abstractions/Modules/Overrides/Configuration/TenantJsonStreamConfigurationProvider.cs b/src/OrchardCore/OrchardCore.Abstractions/Modules/Overrides/Configuration/TenantJsonStreamConfigurationProvider.cs
new file mode 100644
index 00000000000..41d6a3280c6
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Abstractions/Modules/Overrides/Configuration/TenantJsonStreamConfigurationProvider.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IO;
+using OrchardCore.Environment.Shell.Configuration.Internal;
+
+namespace Microsoft.Extensions.Configuration.Json
+{
+ ///
+ /// Loads configuration key/values from a json stream into a provider.
+ ///
+ public class TenantJsonStreamConfigurationProvider : StreamConfigurationProvider
+ {
+ ///
+ /// Constructor.
+ ///
+ /// The .
+ public TenantJsonStreamConfigurationProvider(TenantJsonStreamConfigurationSource source) : base(source) { }
+
+ ///
+ /// Loads json configuration key/values from a stream into a provider.
+ ///
+ /// The json to load configuration data from.
+ public override void Load(Stream stream)
+ {
+ Data = JsonConfigurationParser.Parse(stream);
+ }
+ }
+}
diff --git a/src/OrchardCore/OrchardCore.Abstractions/Modules/Overrides/Configuration/TenantJsonStreamConfigurationSource.cs b/src/OrchardCore/OrchardCore.Abstractions/Modules/Overrides/Configuration/TenantJsonStreamConfigurationSource.cs
new file mode 100644
index 00000000000..45c34f9851c
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Abstractions/Modules/Overrides/Configuration/TenantJsonStreamConfigurationSource.cs
@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.Extensions.Configuration.Json
+{
+ ///
+ /// Represents a JSON file as an .
+ ///
+ public class TenantJsonStreamConfigurationSource : StreamConfigurationSource
+ {
+ ///
+ /// Builds the for this source.
+ ///
+ /// The .
+ /// An
+ public override IConfigurationProvider Build(IConfigurationBuilder builder)
+ => new TenantJsonStreamConfigurationProvider(this);
+ }
+}
diff --git a/src/OrchardCore/OrchardCore.Abstractions/Shell/Configuration/Internal/ConfigurationExtensions.cs b/src/OrchardCore/OrchardCore.Abstractions/Shell/Configuration/Internal/ConfigurationExtensions.cs
new file mode 100644
index 00000000000..9e800cba0ef
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Abstractions/Shell/Configuration/Internal/ConfigurationExtensions.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace OrchardCore.Environment.Shell.Configuration.Internal;
+
+public static class ConfigurationExtensions
+{
+ public static JObject ToJObject(this IConfiguration configuration)
+ {
+ var jToken = ToJToken(configuration);
+ if (jToken is not JObject jObject)
+ {
+ throw new FormatException($"Top level JSON element must be an object. Instead, {jToken.Type} was found.");
+ }
+
+ return jObject;
+ }
+
+ public static JToken ToJToken(this IConfiguration configuration)
+ {
+ JArray jArray = null;
+ JObject jObject = null;
+
+ foreach (var child in configuration.GetChildren())
+ {
+ if (int.TryParse(child.Key, out var index))
+ {
+ if (jObject is not null)
+ {
+ throw new FormatException($"Can't use the numeric key '{child.Key}' inside an object.");
+ }
+
+ jArray ??= new JArray();
+ if (index > jArray.Count)
+ {
+ // Inserting null values is useful to override arrays items,
+ // it allows to keep non null items at the right position.
+ for (var i = jArray.Count; i < index; i++)
+ {
+ jArray.Add(JValue.CreateNull());
+ }
+ }
+
+ if (child.GetChildren().Any())
+ {
+ jArray.Add(ToJToken(child));
+ }
+ else
+ {
+ jArray.Add(child.Value);
+ }
+ }
+ else
+ {
+ if (jArray is not null)
+ {
+ throw new FormatException($"Can't use the non numeric key '{child.Key}' inside an array.");
+ }
+
+ jObject ??= new JObject();
+ if (child.GetChildren().Any())
+ {
+ jObject.Add(child.Key, ToJToken(child));
+ }
+ else
+ {
+ jObject.Add(child.Key, child.Value);
+ }
+ }
+ }
+
+ return jArray as JToken ?? jObject ?? new JObject();
+ }
+
+ public static JObject ToJObject(this IDictionary configurationData)
+ {
+ var configuration = new ConfigurationBuilder()
+ .Add(new UpdatableDataProvider(configurationData))
+ .Build();
+
+ using var disposable = configuration as IDisposable;
+
+ return configuration.ToJObject();
+ }
+
+ public static async Task> ToConfigurationDataAsync(this JObject jConfiguration)
+ {
+ if (jConfiguration is null)
+ {
+ return new Dictionary();
+ }
+
+ var configurationString = await jConfiguration.ToStringAsync(Formatting.None);
+ using var ms = new MemoryStream(Encoding.UTF8.GetBytes(configurationString));
+
+ return await JsonConfigurationParser.ParseAsync(ms);
+ }
+
+ public static async Task ToStringAsync(this JObject jConfiguration, Formatting formatting = Formatting.Indented)
+ {
+ jConfiguration ??= new JObject();
+
+ using var sw = new StringWriter(CultureInfo.InvariantCulture);
+ using var jw = new JsonTextWriter(sw) { Formatting = formatting };
+
+ await jConfiguration.WriteToAsync(jw);
+
+ return sw.ToString();
+ }
+}
diff --git a/src/OrchardCore/OrchardCore.Abstractions/Shell/Configuration/Internal/JsonConfigurationParser.cs b/src/OrchardCore/OrchardCore.Abstractions/Shell/Configuration/Internal/JsonConfigurationParser.cs
new file mode 100644
index 00000000000..615e86c6fe4
--- /dev/null
+++ b/src/OrchardCore/OrchardCore.Abstractions/Shell/Configuration/Internal/JsonConfigurationParser.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+
+#nullable enable
+
+namespace OrchardCore.Environment.Shell.Configuration.Internal;
+
+public sealed class JsonConfigurationParser
+{
+ private JsonConfigurationParser() { }
+
+ private readonly Dictionary _data = new(StringComparer.OrdinalIgnoreCase);
+ private readonly Stack _paths = new();
+
+ public static IDictionary Parse(Stream input)
+ => new JsonConfigurationParser().ParseStream(input);
+
+ public static Task> ParseAsync(Stream input)
+ => new JsonConfigurationParser().ParseStreamAsync(input);
+
+ private IDictionary ParseStream(Stream input)
+ {
+ var jsonDocumentOptions = new JsonDocumentOptions
+ {
+ CommentHandling = JsonCommentHandling.Skip,
+ AllowTrailingCommas = true,
+ };
+
+ try
+ {
+ using (var reader = new StreamReader(input))
+ using (var doc = JsonDocument.Parse(reader.ReadToEnd(), jsonDocumentOptions))
+ {
+ if (doc.RootElement.ValueKind != JsonValueKind.Object)
+ {
+ throw new FormatException($"Top-level JSON element must be an object. Instead, '{doc.RootElement.ValueKind}' was found.");
+ }
+
+ VisitObjectElement(doc.RootElement);
+ }
+
+ return _data;
+ }
+ catch (JsonException e)
+ {
+ throw new FormatException("Could not parse the JSON document.", e);
+ }
+ }
+
+ private async Task> ParseStreamAsync(Stream input)
+ {
+ var jsonDocumentOptions = new JsonDocumentOptions
+ {
+ CommentHandling = JsonCommentHandling.Skip,
+ AllowTrailingCommas = true,
+ };
+
+ try
+ {
+ using (var doc = await JsonDocument.ParseAsync(input, jsonDocumentOptions))
+ {
+ if (doc.RootElement.ValueKind != JsonValueKind.Object)
+ {
+ throw new FormatException($"Top-level JSON element must be an object. Instead, '{doc.RootElement.ValueKind}' was found.");
+ }
+
+ VisitObjectElement(doc.RootElement);
+ }
+
+ return _data;
+ }
+ catch (JsonException e)
+ {
+ throw new FormatException("Could not parse the JSON document.", e);
+ }
+ }
+
+ private void VisitObjectElement(JsonElement element)
+ {
+ var isEmpty = true;
+
+ foreach (var property in element.EnumerateObject())
+ {
+ isEmpty = false;
+ EnterContext(property.Name);
+ VisitValue(property.Value);
+ ExitContext();
+ }
+
+ SetNullIfElementIsEmpty(isEmpty);
+ }
+
+ private void VisitArrayElement(JsonElement element)
+ {
+ var index = 0;
+
+ foreach (var arrayElement in element.EnumerateArray())
+ {
+ EnterContext(index.ToString());
+ VisitValue(arrayElement, visitArray: true);
+ ExitContext();
+ index++;
+ }
+
+ SetNullIfElementIsEmpty(isEmpty: index == 0);
+ }
+
+ private void SetNullIfElementIsEmpty(bool isEmpty)
+ {
+ if (isEmpty && _paths.Count > 0)
+ {
+ _data[_paths.Peek()] = null;
+ }
+ }
+
+ private void VisitValue(JsonElement value, bool visitArray = false)
+ {
+ Debug.Assert(_paths.Count > 0);
+
+ switch (value.ValueKind)
+ {
+ case JsonValueKind.Object:
+ VisitObjectElement(value);
+ break;
+
+ case JsonValueKind.Array:
+ VisitArrayElement(value);
+ break;
+
+ case JsonValueKind.Number:
+ case JsonValueKind.String:
+ case JsonValueKind.True:
+ case JsonValueKind.False:
+ case JsonValueKind.Null:
+
+ // Skipping null values is useful to override array items,
+ // it allows to keep non null items at the right position.
+ if (visitArray && value.ValueKind == JsonValueKind.Null)
+ {
+ break;
+ }
+
+ var key = _paths.Peek();
+ if (_data.ContainsKey(key))
+ {
+ throw new FormatException($"A duplicate key '{key}' was found.");
+ }
+ _data[key] = value.ToString();
+ break;
+
+ default:
+ throw new FormatException($"Unsupported JSON token '{value.ValueKind}' was found.");
+ }
+ }
+
+ private void EnterContext(string context) =>
+ _paths.Push(_paths.Count > 0 ?
+ _paths.Peek() + ConfigurationPath.KeyDelimiter + context :
+ context);
+
+ private void ExitContext() => _paths.Pop();
+}
diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellConfigurationSources.cs b/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellConfigurationSources.cs
index 2012b2265bd..81627d2c1bc 100644
--- a/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellConfigurationSources.cs
+++ b/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellConfigurationSources.cs
@@ -10,6 +10,7 @@
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Builders;
using OrchardCore.Environment.Shell.Configuration;
+using OrchardCore.Environment.Shell.Configuration.Internal;
using OrchardCore.Shells.Database.Extensions;
using OrchardCore.Shells.Database.Models;
using YesSql;
@@ -76,7 +77,8 @@ public async Task AddSourcesAsync(string tenant, IConfigurationBuilder builder)
var configuration = configurations.GetValue(tenant) as JObject;
if (configuration is not null)
{
- builder.AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(configuration.ToString(Formatting.None))));
+ var configurationString = await configuration.ToStringAsync(Formatting.None);
+ builder.AddTenantJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(configurationString)));
}
}
@@ -100,22 +102,23 @@ public async Task SaveAsync(string tenant, IDictionary data)
configurations = new JObject();
}
- var config = configurations.GetValue(tenant) as JObject ?? new JObject();
+ var configData = await (configurations
+ .GetValue(tenant) as JObject)
+ .ToConfigurationDataAsync();
foreach (var key in data.Keys)
{
if (data[key] is not null)
{
- config[key] = data[key];
+ configData[key] = data[key];
}
else
{
- config.Remove(key);
+ configData.Remove(key);
}
}
- configurations[tenant] = config;
-
+ configurations[tenant] = configData.ToJObject();
document.ShellConfigurations = configurations;
session.Save(document, checkConcurrency: true);
diff --git a/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellsSettingsSources.cs b/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellsSettingsSources.cs
index ace533cc127..1bf55236c8f 100644
--- a/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellsSettingsSources.cs
+++ b/src/OrchardCore/OrchardCore.Infrastructure/Shells.Database/Configuration/DatabaseShellsSettingsSources.cs
@@ -10,6 +10,7 @@
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Builders;
using OrchardCore.Environment.Shell.Configuration;
+using OrchardCore.Environment.Shell.Configuration.Internal;
using OrchardCore.Shells.Database.Extensions;
using OrchardCore.Shells.Database.Models;
using YesSql;
@@ -44,7 +45,8 @@ public async Task AddSourcesAsync(IConfigurationBuilder builder)
var document = await GetDocumentAsync();
if (document.ShellsSettings is not null)
{
- builder.AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(document.ShellsSettings.ToString(Formatting.None))));
+ var shellsSettingsString = await document.ShellsSettings.ToStringAsync(Formatting.None);
+ builder.AddTenantJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(shellsSettingsString)));
}
}
@@ -53,12 +55,9 @@ public async Task AddSourcesAsync(string tenant, IConfigurationBuilder builder)
var document = await GetDocumentAsync();
if (document.ShellsSettings is not null && document.ShellsSettings.ContainsKey(tenant))
{
- var shellSettings = new JObject
- {
- [tenant] = document.ShellsSettings[tenant]
- };
-
- builder.AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(shellSettings.ToString(Formatting.None))));
+ var shellSettings = new JObject { [tenant] = document.ShellsSettings[tenant] };
+ var shellSettingsString = await shellSettings.ToStringAsync(Formatting.None);
+ builder.AddTenantJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(shellSettingsString)));
}
}
diff --git a/src/OrchardCore/OrchardCore.Shells.Azure/Configuration/BlobShellConfigurationSources.cs b/src/OrchardCore/OrchardCore.Shells.Azure/Configuration/BlobShellConfigurationSources.cs
index 1a7a06fb744..bf79e004054 100644
--- a/src/OrchardCore/OrchardCore.Shells.Azure/Configuration/BlobShellConfigurationSources.cs
+++ b/src/OrchardCore/OrchardCore.Shells.Azure/Configuration/BlobShellConfigurationSources.cs
@@ -1,12 +1,13 @@
using System.Collections.Generic;
using System.IO;
+using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Configuration;
+using OrchardCore.Environment.Shell.Configuration.Internal;
using OrchardCore.FileStorage;
using OrchardCore.Shells.Azure.Services;
@@ -46,10 +47,11 @@ public async Task AddSourcesAsync(string tenant, IConfigurationBuilder builder)
fileInfo = await _shellsFileStore.GetFileInfoAsync(appSettings);
}
}
+
if (fileInfo != null)
{
var stream = await _shellsFileStore.GetFileStreamAsync(appSettings);
- builder.AddJsonStream(stream);
+ builder.AddTenantJsonStream(stream);
}
}
@@ -57,41 +59,34 @@ public async Task SaveAsync(string tenant, IDictionary data)
{
var appsettings = IFileStoreExtensions.Combine(null, _container, tenant, "appsettings.json");
- JObject config;
var fileInfo = await _shellsFileStore.GetFileInfoAsync(appsettings);
+ IDictionary configData;
if (fileInfo != null)
{
using var stream = await _shellsFileStore.GetFileStreamAsync(appsettings);
- using var streamReader = new StreamReader(stream);
- using var reader = new JsonTextReader(streamReader);
- config = await JObject.LoadAsync(reader);
+ configData = await JsonConfigurationParser.ParseAsync(stream);
}
else
{
- config = new JObject();
+ configData = new Dictionary();
}
foreach (var key in data.Keys)
{
- if (data[key] != null)
+ if (data[key] is not null)
{
- config[key] = data[key];
+ configData[key] = data[key];
}
else
{
- config.Remove(key);
+ configData.Remove(key);
}
}
- using var memoryStream = new MemoryStream();
- using var streamWriter = new StreamWriter(memoryStream);
- using var jsonWriter = new JsonTextWriter(streamWriter) { Formatting = Formatting.Indented };
-
- await config.WriteToAsync(jsonWriter);
- await jsonWriter.FlushAsync();
+ var configurationString = await configData.ToJObject().ToStringAsync(Formatting.None);
+ using var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(configurationString));
- memoryStream.Position = 0;
await _shellsFileStore.CreateFileFromStreamAsync(appsettings, memoryStream);
}
diff --git a/src/OrchardCore/OrchardCore.Shells.Azure/Configuration/BlobShellsConfigurationSources.cs b/src/OrchardCore/OrchardCore.Shells.Azure/Configuration/BlobShellsConfigurationSources.cs
index 48599334296..25b028162c2 100644
--- a/src/OrchardCore/OrchardCore.Shells.Azure/Configuration/BlobShellsConfigurationSources.cs
+++ b/src/OrchardCore/OrchardCore.Shells.Azure/Configuration/BlobShellsConfigurationSources.cs
@@ -45,7 +45,7 @@ public async Task AddSourcesAsync(IConfigurationBuilder builder)
if (appSettingsFileInfo != null)
{
var stream = await _shellsFileStore.GetFileStreamAsync("appsettings.json");
- builder.AddJsonStream(stream);
+ builder.AddTenantJsonStream(stream);
}
var environmentAppSettingsFileName = $"appsettings.{_environment}.json";
@@ -65,7 +65,7 @@ public async Task AddSourcesAsync(IConfigurationBuilder builder)
if (environmentAppSettingsFileInfo != null)
{
var stream = await _shellsFileStore.GetFileStreamAsync(environmentAppSettingsFileName);
- builder.AddJsonStream(stream);
+ builder.AddTenantJsonStream(stream);
}
}
diff --git a/src/OrchardCore/OrchardCore.Shells.Azure/Configuration/BlobShellsSettingsSources.cs b/src/OrchardCore/OrchardCore.Shells.Azure/Configuration/BlobShellsSettingsSources.cs
index b4e3ce714b4..67b0d03835c 100644
--- a/src/OrchardCore/OrchardCore.Shells.Azure/Configuration/BlobShellsSettingsSources.cs
+++ b/src/OrchardCore/OrchardCore.Shells.Azure/Configuration/BlobShellsSettingsSources.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
+using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
@@ -7,6 +8,7 @@
using Newtonsoft.Json.Linq;
using OrchardCore.Environment.Shell;
using OrchardCore.Environment.Shell.Configuration;
+using OrchardCore.Environment.Shell.Configuration.Internal;
using OrchardCore.Shells.Azure.Services;
namespace OrchardCore.Shells.Azure.Configuration
@@ -47,7 +49,7 @@ public async Task AddSourcesAsync(IConfigurationBuilder builder)
if (fileInfo != null)
{
var stream = await _shellsFileStore.GetFileStreamAsync(TenantsBlobName);
- builder.AddJsonStream(stream);
+ builder.AddTenantJsonStream(stream);
}
}
@@ -63,8 +65,8 @@ public async Task SaveAsync(string tenant, IDictionary data)
{
using var stream = await _shellsFileStore.GetFileStreamAsync(TenantsBlobName);
using var streamReader = new StreamReader(stream);
- using var reader = new JsonTextReader(streamReader);
- tenantsSettings = await JObject.LoadAsync(reader);
+ using var jsonReader = new JsonTextReader(streamReader);
+ tenantsSettings = await JObject.LoadAsync(jsonReader);
}
else
{
@@ -87,14 +89,9 @@ public async Task SaveAsync(string tenant, IDictionary data)
tenantsSettings[tenant] = settings;
- using var memoryStream = new MemoryStream();
- using var streamWriter = new StreamWriter(memoryStream);
- using var jsonWriter = new JsonTextWriter(streamWriter) { Formatting = Formatting.Indented };
+ var tenantsSettingsString = await tenantsSettings.ToStringAsync(Formatting.None);
+ using var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(tenantsSettingsString));
- await tenantsSettings.WriteToAsync(jsonWriter);
- await jsonWriter.FlushAsync();
-
- memoryStream.Position = 0;
await _shellsFileStore.CreateFileFromStreamAsync(TenantsBlobName, memoryStream);
}
@@ -114,13 +111,9 @@ public async Task RemoveAsync(string tenant)
tenantsSettings.Remove(tenant);
- using var memoryStream = new MemoryStream();
- using var streamWriter = new StreamWriter(memoryStream);
- using var jsonWriter = new JsonTextWriter(streamWriter) { Formatting = Formatting.Indented };
+ var tenantsSettingsString = await tenantsSettings.ToStringAsync(Formatting.None);
+ using var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(tenantsSettingsString));
- await tenantsSettings.WriteToAsync(jsonWriter);
- await jsonWriter.FlushAsync();
- memoryStream.Position = 0;
await _shellsFileStore.CreateFileFromStreamAsync(TenantsBlobName, memoryStream);
}
}
diff --git a/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/JsonConfigurationFileParser.cs b/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/JsonConfigurationFileParser.cs
deleted file mode 100644
index 770fcb255cc..00000000000
--- a/src/OrchardCore/OrchardCore/Modules/Overrides/Configuration/JsonConfigurationFileParser.cs
+++ /dev/null
@@ -1,125 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Text.Json;
-
-#nullable enable
-
-namespace Microsoft.Extensions.Configuration.Json
-{
- internal sealed class JsonConfigurationFileParser
- {
- private JsonConfigurationFileParser() { }
-
- private readonly Dictionary _data = new Dictionary(StringComparer.OrdinalIgnoreCase);
- private readonly Stack _paths = new Stack();
-
- public static IDictionary Parse(Stream input)
- => new JsonConfigurationFileParser().ParseStream(input);
-
- private Dictionary ParseStream(Stream input)
- {
- var jsonDocumentOptions = new JsonDocumentOptions
- {
- CommentHandling = JsonCommentHandling.Skip,
- AllowTrailingCommas = true,
- };
-
- using (var reader = new StreamReader(input))
- using (JsonDocument doc = JsonDocument.Parse(reader.ReadToEnd(), jsonDocumentOptions))
- {
- if (doc.RootElement.ValueKind != JsonValueKind.Object)
- {
- // throw new FormatException(SR.Format(SR.Error_InvalidTopLevelJSONElement, doc.RootElement.ValueKind));
- throw new FormatException($"Top-level JSON element must be an object. Instead, '{doc.RootElement.ValueKind}' was found.");
- }
- VisitObjectElement(doc.RootElement);
- }
-
- return _data;
- }
-
- private void VisitObjectElement(JsonElement element)
- {
- var isEmpty = true;
-
- foreach (JsonProperty property in element.EnumerateObject())
- {
- isEmpty = false;
- EnterContext(property.Name);
- VisitValue(property.Value);
- ExitContext();
- }
-
- SetNullIfElementIsEmpty(isEmpty);
- }
-
- private void VisitArrayElement(JsonElement element)
- {
- int index = 0;
-
- foreach (JsonElement arrayElement in element.EnumerateArray())
- {
- EnterContext(index.ToString());
- VisitValue(arrayElement);
- ExitContext();
- index++;
- }
-
- SetNullIfElementIsEmpty(isEmpty: index == 0);
- }
-
- private void SetNullIfElementIsEmpty(bool isEmpty)
- {
- if (isEmpty && _paths.Count > 0)
- {
- _data[_paths.Peek()] = null;
- }
- }
-
- private void VisitValue(JsonElement value)
- {
- Debug.Assert(_paths.Count > 0);
-
- switch (value.ValueKind)
- {
- case JsonValueKind.Object:
- VisitObjectElement(value);
- break;
-
- case JsonValueKind.Array:
- VisitArrayElement(value);
- break;
-
- case JsonValueKind.Number:
- case JsonValueKind.String:
- case JsonValueKind.True:
- case JsonValueKind.False:
- case JsonValueKind.Null:
- string key = _paths.Peek();
- if (_data.ContainsKey(key))
- {
- // throw new FormatException(SR.Format(SR.Error_KeyIsDuplicated, key));
- throw new FormatException($"A duplicate key '{key}' was found.");
- }
- _data[key] = value.ToString();
- break;
-
- default:
- // throw new FormatException(SR.Format(SR.Error_UnsupportedJSONToken, value.ValueKind));
- throw new FormatException($"Unsupported JSON token '{value.ValueKind}' was found.");
- }
- }
-
- private void EnterContext(string context) =>
- _paths.Push(_paths.Count > 0 ?
- _paths.Peek() + ConfigurationPath.KeyDelimiter + context :
- context);
-
- private void ExitContext() => _paths.Pop();
- }
-}
diff --git a/src/OrchardCore/OrchardCore/Shell/Configuration/ShellConfigurationSources.cs b/src/OrchardCore/OrchardCore/Shell/Configuration/ShellConfigurationSources.cs
index 0bd0466c5f1..2c287f0ec52 100644
--- a/src/OrchardCore/OrchardCore/Shell/Configuration/ShellConfigurationSources.cs
+++ b/src/OrchardCore/OrchardCore/Shell/Configuration/ShellConfigurationSources.cs
@@ -5,7 +5,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
+using OrchardCore.Environment.Shell.Configuration.Internal;
namespace OrchardCore.Environment.Shell.Configuration
{
@@ -24,9 +24,7 @@ public ShellConfigurationSources(IOptions shellOptions, ILogger data)
var tenantFolder = Path.Combine(_container, tenant);
var appsettings = Path.Combine(tenantFolder, "appsettings.json");
- JObject config;
+ IDictionary configData;
if (File.Exists(appsettings))
{
- using var streamReader = File.OpenText(appsettings);
- using var jsonReader = new JsonTextReader(streamReader);
- config = await JObject.LoadAsync(jsonReader);
+ using var stream = File.OpenRead(appsettings);
+ configData = await JsonConfigurationParser.ParseAsync(stream);
}
else
{
- config = new JObject();
+ configData = new Dictionary();
}
foreach (var key in data.Keys)
{
- if (data[key] != null)
+ if (data[key] is not null)
{
- config[key] = data[key];
+ configData[key] = data[key];
}
else
{
- config.Remove(key);
+ configData.Remove(key);
}
}
@@ -63,7 +60,8 @@ public async Task SaveAsync(string tenant, IDictionary data)
using var streamWriter = File.CreateText(appsettings);
using var jsonWriter = new JsonTextWriter(streamWriter) { Formatting = Formatting.Indented };
- await config.WriteToAsync(jsonWriter);
+
+ await configData.ToJObject().WriteToAsync(jsonWriter);
}
public Task RemoveAsync(string tenant)
diff --git a/src/OrchardCore/OrchardCore/Shell/Configuration/ShellsSettingsSources.cs b/src/OrchardCore/OrchardCore/Shell/Configuration/ShellsSettingsSources.cs
index 96824032eee..bb12a3e8783 100644
--- a/src/OrchardCore/OrchardCore/Shell/Configuration/ShellsSettingsSources.cs
+++ b/src/OrchardCore/OrchardCore/Shell/Configuration/ShellsSettingsSources.cs
@@ -32,6 +32,7 @@ public async Task SaveAsync(string tenant, IDictionary data)
{
using var streamReader = File.OpenText(_tenants);
using var jsonReader = new JsonTextReader(streamReader);
+
tenantsSettings = await JObject.LoadAsync(jsonReader);
}
else
@@ -43,7 +44,7 @@ public async Task SaveAsync(string tenant, IDictionary data)
foreach (var key in data.Keys)
{
- if (data[key] != null)
+ if (data[key] is not null)
{
settings[key] = data[key];
}
@@ -57,6 +58,7 @@ public async Task SaveAsync(string tenant, IDictionary data)
using var streamWriter = File.CreateText(_tenants);
using var jsonWriter = new JsonTextWriter(streamWriter) { Formatting = Formatting.Indented };
+
await tenantsSettings.WriteToAsync(jsonWriter);
}
@@ -75,6 +77,7 @@ public async Task RemoveAsync(string tenant)
using var streamWriter = File.CreateText(_tenants);
using var jsonWriter = new JsonTextWriter(streamWriter) { Formatting = Formatting.Indented };
+
await tenantsSettings.WriteToAsync(jsonWriter);
}
}
diff --git a/src/OrchardCore/OrchardCore/Shell/ShellSettingsManager.cs b/src/OrchardCore/OrchardCore/Shell/ShellSettingsManager.cs
index 541b82ee3a9..719e966e6dc 100644
--- a/src/OrchardCore/OrchardCore/Shell/ShellSettingsManager.cs
+++ b/src/OrchardCore/OrchardCore/Shell/ShellSettingsManager.cs
@@ -179,21 +179,21 @@ public async Task SaveSettingsAsync(ShellSettings settings)
await _tenantsSettingsSources.SaveAsync(settings.Name, tenantSettings.ToObject>());
- var tenantConfig = new JObject();
-
- var sections = settings.ShellConfiguration.GetChildren()
- .Where(s => !s.GetChildren().Any())
- .ToArray();
-
- foreach (var section in sections)
+ var tenantConfig = new Dictionary();
+ foreach (var config in settings.ShellConfiguration.AsEnumerable())
{
- if (settings[section.Key] != configuration[section.Key])
+ if (settings.ShellConfiguration.GetSection(config.Key).GetChildren().Any())
+ {
+ continue;
+ }
+
+ if (settings[config.Key] != configuration[config.Key])
{
- tenantConfig[section.Key] = settings[section.Key];
+ tenantConfig[config.Key] = settings[config.Key];
}
else
{
- tenantConfig[section.Key] = null;
+ tenantConfig[config.Key] = null;
}
}
@@ -202,7 +202,7 @@ public async Task SaveSettingsAsync(ShellSettings settings)
await _tenantConfigSemaphore.WaitAsync();
try
{
- await _tenantConfigSources.SaveAsync(settings.Name, tenantConfig.ToObject>());
+ await _tenantConfigSources.SaveAsync(settings.Name, tenantConfig);
}
finally
{