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 {