From 17af9686d4806175d794094fe172d08220cad0a2 Mon Sep 17 00:00:00 2001 From: Andreas Date: Fri, 12 Jun 2026 07:24:09 +0200 Subject: [PATCH] Removed inheritance of `JsonSerializerContext` This commit adds a new `JsonOptionsProvider` allowing to combine `JsonSerialzeContext`. However, this will require unsafe code. Fixed #449 --- .../SqlDatabaseSourceGenerationContext.cs | 3 +- .../SqliteDatabaseSourceGenerationContext.cs | 3 +- .../LicenseSourceGenerationContext.cs | 2 +- src/SharedMauiCoreLibrary.Test/CoreTests.cs | 30 +++++++++- .../Models/TestModel.cs | 27 +++++++++ .../SourceGeneration/TestContext.cs | 11 ++++ .../MauiSourceGenerationContext.cs | 2 +- .../Utilities/JsonConvertHelper.cs | 60 +++++++++++++++++-- .../Utilities/JsonOptionsProvider.cs | 31 ++++++++++ 9 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 src/SharedMauiCoreLibrary.Test/Models/TestModel.cs create mode 100644 src/SharedMauiCoreLibrary.Test/SourceGeneration/TestContext.cs create mode 100644 src/SharedNetCoreLibrary/Utilities/JsonOptionsProvider.cs diff --git a/src/SharedMauiCoreLibrary.Database.SQL/SourceGeneration/SqlDatabaseSourceGenerationContext.cs b/src/SharedMauiCoreLibrary.Database.SQL/SourceGeneration/SqlDatabaseSourceGenerationContext.cs index a6ac939..e3df19f 100644 --- a/src/SharedMauiCoreLibrary.Database.SQL/SourceGeneration/SqlDatabaseSourceGenerationContext.cs +++ b/src/SharedMauiCoreLibrary.Database.SQL/SourceGeneration/SqlDatabaseSourceGenerationContext.cs @@ -1,10 +1,9 @@ using AndreasReitberger.Shared.Core.Database.Service; -using AndreasReitberger.Shared.Core.SourceGeneration; namespace AndreasReitberger.Shared.Core.Database.SourceGeneration { [JsonSerializable(typeof(SqlDatabaseService))] [JsonSourceGenerationOptions(WriteIndented = true)] - public partial class SqlDatabaseSourceGenerationContext : MauiSourceGenerationContext { } + public partial class SqlDatabaseSourceGenerationContext : JsonSerializerContext { } } diff --git a/src/SharedMauiCoreLibrary.Database.SQLite/SourceGeneration/SqliteDatabaseSourceGenerationContext.cs b/src/SharedMauiCoreLibrary.Database.SQLite/SourceGeneration/SqliteDatabaseSourceGenerationContext.cs index fc7047d..1b6c883 100644 --- a/src/SharedMauiCoreLibrary.Database.SQLite/SourceGeneration/SqliteDatabaseSourceGenerationContext.cs +++ b/src/SharedMauiCoreLibrary.Database.SQLite/SourceGeneration/SqliteDatabaseSourceGenerationContext.cs @@ -1,10 +1,9 @@ using AndreasReitberger.Shared.Core.Database.Service; -using AndreasReitberger.Shared.Core.SourceGeneration; namespace AndreasReitberger.Shared.Core.Database.SourceGeneration { [JsonSerializable(typeof(SqliteDatabaseService))] [JsonSourceGenerationOptions(WriteIndented = true)] - public partial class SqliteDatabaseSourceGenerationContext : MauiSourceGenerationContext { } + public partial class SqliteDatabaseSourceGenerationContext : JsonSerializerContext { } } diff --git a/src/SharedMauiCoreLibrary.Licensing/SourceGeneration/LicenseSourceGenerationContext.cs b/src/SharedMauiCoreLibrary.Licensing/SourceGeneration/LicenseSourceGenerationContext.cs index 5e9fde1..c353581 100644 --- a/src/SharedMauiCoreLibrary.Licensing/SourceGeneration/LicenseSourceGenerationContext.cs +++ b/src/SharedMauiCoreLibrary.Licensing/SourceGeneration/LicenseSourceGenerationContext.cs @@ -21,6 +21,6 @@ namespace AndreasReitberger.Shared.Core.Licensing.SourceGeneration [JsonSerializable(typeof(WooActivationResponse[]))] [JsonSerializable(typeof(WooCodeVersionResponse[]))] [JsonSourceGenerationOptions(WriteIndented = true)] - public partial class LicenseSourceGenerationContext : CoreSourceGenerationContext { } + public partial class LicenseSourceGenerationContext : JsonSerializerContext { } } diff --git a/src/SharedMauiCoreLibrary.Test/CoreTests.cs b/src/SharedMauiCoreLibrary.Test/CoreTests.cs index 0aec8a6..b8b606d 100644 --- a/src/SharedMauiCoreLibrary.Test/CoreTests.cs +++ b/src/SharedMauiCoreLibrary.Test/CoreTests.cs @@ -1,4 +1,6 @@ -using AndreasReitberger.Shared.Core.Utilities; +using AndreasReitberger.Shared.Core.SourceGeneration; +using AndreasReitberger.Shared.Core.Utilities; +using SharedMauiCoreLibrary.Test.Models; namespace SharedMauiCoreLibrary.Test; @@ -108,4 +110,30 @@ public void UsePasswordDoubleEncryptionTest() Assert.Fail(ex.Message); } } + + [Test] + public void JsonSerializerCombineTest() + { + try + { + TestModel tm = new() + { + Name = "test", + IsActive = true, + Start = DateTime.Now.AddHours(-1), + End = DateTime.Now, + Items = ["Item1", "Item2", "Item3"] + }; + var combinedOptions = JsonOptionsProvider.CombinedOptions(SourceGeneration.TestContext.Default, CoreSourceGenerationContext.Default, MauiSourceGenerationContext.Default); + string? json = JsonConvertHelper.ToSettingsString(tm, options: combinedOptions); + Assert.That(!string.IsNullOrEmpty(json)); + + TestModel? tm2 = JsonConvertHelper.ToObject(json, options: combinedOptions); + Assert.That(tm2, Is.Not.Null); + } + catch (Exception ex) + { + Assert.Fail(ex.Message); + } + } } diff --git a/src/SharedMauiCoreLibrary.Test/Models/TestModel.cs b/src/SharedMauiCoreLibrary.Test/Models/TestModel.cs new file mode 100644 index 0000000..aee3b24 --- /dev/null +++ b/src/SharedMauiCoreLibrary.Test/Models/TestModel.cs @@ -0,0 +1,27 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System.Collections.ObjectModel; + +namespace SharedMauiCoreLibrary.Test.Models +{ + public partial class TestModel : ObservableObject + { + #region Properties + + [ObservableProperty] + public partial string Name { get; set; } = string.Empty; + + [ObservableProperty] + public partial bool IsActive { get; set; } + + [ObservableProperty] + public partial DateTimeOffset Start { get; set; } + + [ObservableProperty] + public partial DateTimeOffset End { get; set; } + + [ObservableProperty] + public partial ObservableCollection Items { get; set; } = []; + + #endregion + } +} diff --git a/src/SharedMauiCoreLibrary.Test/SourceGeneration/TestContext.cs b/src/SharedMauiCoreLibrary.Test/SourceGeneration/TestContext.cs new file mode 100644 index 0000000..2b7ca90 --- /dev/null +++ b/src/SharedMauiCoreLibrary.Test/SourceGeneration/TestContext.cs @@ -0,0 +1,11 @@ +using SharedMauiCoreLibrary.Test.Models; +using System.Text.Json.Serialization; + +namespace SharedMauiCoreLibrary.Test.SourceGeneration +{ + [JsonSerializable(typeof(TestModel))] + [JsonSourceGenerationOptions(WriteIndented = true)] + public partial class TestContext : JsonSerializerContext + { + } +} diff --git a/src/SharedMauiCoreLibrary/SourceGeneration/MauiSourceGenerationContext.cs b/src/SharedMauiCoreLibrary/SourceGeneration/MauiSourceGenerationContext.cs index 9685b10..3d2b1ac 100644 --- a/src/SharedMauiCoreLibrary/SourceGeneration/MauiSourceGenerationContext.cs +++ b/src/SharedMauiCoreLibrary/SourceGeneration/MauiSourceGenerationContext.cs @@ -18,6 +18,6 @@ namespace AndreasReitberger.Shared.Core.SourceGeneration [JsonSerializable(typeof(ThemeColorInfo))] [JsonSerializable(typeof(ColorPickerElement))] [JsonSourceGenerationOptions(WriteIndented = true)] - public partial class MauiSourceGenerationContext : CoreSourceGenerationContext { } + public partial class MauiSourceGenerationContext : JsonSerializerContext { } } diff --git a/src/SharedNetCoreLibrary/Utilities/JsonConvertHelper.cs b/src/SharedNetCoreLibrary/Utilities/JsonConvertHelper.cs index bbe6d30..d24c17b 100644 --- a/src/SharedNetCoreLibrary/Utilities/JsonConvertHelper.cs +++ b/src/SharedNetCoreLibrary/Utilities/JsonConvertHelper.cs @@ -1,21 +1,65 @@  + +#if NET6_0_OR_GREATER +using System.Diagnostics.CodeAnalysis; +#endif + namespace AndreasReitberger.Shared.Core.Utilities { public partial class JsonConvertHelper { #region Converts - public static T? ToObject(string? jsonString, T? defaultValue = default, Action? OnError = null, JsonSerializerContext? settings = null) + public static T? ToObject(string? jsonString, T? defaultValue = default, Action? OnError = null, JsonSerializerContext? context = null) { try { if (jsonString is null) return defaultValue; - settings ??= CoreSourceGenerationContext.Default; + context ??= CoreSourceGenerationContext.Default; // Check if it is saved as plain string. If so, just return the string if (typeof(T) == typeof(string) && !(jsonString.StartsWith('"') && jsonString.EndsWith('"'))) return (T)Convert.ChangeType(jsonString, typeof(T)); else - return (T?)JsonSerializer.Deserialize(jsonString, typeof(T), settings) ?? defaultValue; + return (T?)JsonSerializer.Deserialize(jsonString, typeof(T), context) ?? defaultValue; + } + catch (Exception exc) + { + OnError?.Invoke(exc); + return defaultValue; + } + } + +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("This function is not AOT safe. Use the `JsonSerializerContext` instead")] + [RequiresDynamicCode("This function is not AOT safe. Use the `JsonSerializerContext` instead")] +#endif + public static T? ToObject(string? jsonString, JsonSerializerOptions? options, T? defaultValue = default, Action? OnError = null) + { + try + { + if (jsonString is null) + return defaultValue; + options ??= CoreSourceGenerationContext.Default.Options; + // Check if it is saved as plain string. If so, just return the string + if (typeof(T) == typeof(string) && !(jsonString.StartsWith('"') && jsonString.EndsWith('"'))) + return (T)Convert.ChangeType(jsonString, typeof(T)); + else + return JsonSerializer.Deserialize(jsonString, options) ?? defaultValue; + } + catch (Exception exc) + { + OnError?.Invoke(exc); + return defaultValue; + } + } + + public static string? ToSettingsString(T? settingsObject, string? defaultValue = default, Action? OnError = null, JsonSerializerContext? context = null) + { + try + { + if (settingsObject is null) return defaultValue; + context ??= CoreSourceGenerationContext.Default; + return JsonSerializer.Serialize(settingsObject, typeof(T), context) ?? defaultValue; } catch (Exception exc) { @@ -24,13 +68,17 @@ public partial class JsonConvertHelper } } - public static string? ToSettingsString(T? settingsObject, string? defaultValue = default, Action? OnError = null, JsonSerializerContext? settings = null) +#if NET6_0_OR_GREATER + [RequiresUnreferencedCode("This function is not AOT safe. Use the `JsonSerializerContext` instead")] + [RequiresDynamicCode("This function is not AOT safe. Use the `JsonSerializerContext` instead")] +#endif + public static string? ToSettingsString(T? settingsObject, JsonSerializerOptions? options, string? defaultValue = default, Action? OnError = null) { try { if (settingsObject is null) return defaultValue; - settings ??= CoreSourceGenerationContext.Default; - return JsonSerializer.Serialize(settingsObject, typeof(T), settings) ?? defaultValue; + options ??= CoreSourceGenerationContext.Default.Options; + return JsonSerializer.Serialize(settingsObject, options) ?? defaultValue; } catch (Exception exc) { diff --git a/src/SharedNetCoreLibrary/Utilities/JsonOptionsProvider.cs b/src/SharedNetCoreLibrary/Utilities/JsonOptionsProvider.cs new file mode 100644 index 0000000..5754735 --- /dev/null +++ b/src/SharedNetCoreLibrary/Utilities/JsonOptionsProvider.cs @@ -0,0 +1,31 @@ + +using System.Text.Json.Serialization.Metadata; + +namespace AndreasReitberger.Shared.Core.Utilities +{ + public partial class JsonOptionsProvider + { + #region Converts + + public static JsonSerializerOptions CombinedOptions(params IJsonTypeInfoResolver[] resolvers) + { + return new JsonSerializerOptions + { + TypeInfoResolver = CombineResolvers(resolvers) + }; + } + + public static IJsonTypeInfoResolver? CombineResolvers(params IJsonTypeInfoResolver[] resolvers) + { + try + { + return JsonTypeInfoResolver.Combine(resolvers); + } + catch (Exception) + { + return null; + } + } + #endregion + } +} \ No newline at end of file