diff --git a/fern/pages/changelogs/csharp-model/2025-03-02.mdx b/fern/pages/changelogs/csharp-model/2025-03-02.mdx new file mode 100644 index 00000000000..e00cb33df18 --- /dev/null +++ b/fern/pages/changelogs/csharp-model/2025-03-02.mdx @@ -0,0 +1,3 @@ +## 0.0.2 +**`(internal):`** Upgrade to IR version 56. + diff --git a/generators/csharp/codegen/src/AsIs.ts b/generators/csharp/codegen/src/AsIs.ts index c9f1bc73d13..c00b7ddbef2 100644 --- a/generators/csharp/codegen/src/AsIs.ts +++ b/generators/csharp/codegen/src/AsIs.ts @@ -5,6 +5,7 @@ export const COLLECTION_ITEM_SERIALIZER_CLASS_NAME = "CollectionItemSerializer"; export const DATETIME_SERIALIZER_CLASS_NAME = "DateTimeSerializer"; export const CONSTANTS_CLASS_NAME = "Constants"; export const JSON_UTILS_CLASS_NAME = "JsonUtils"; +export const JSON_ACCESS_ATTRIBUTE_NAME = "JsonAccess"; export const AsIsFiles = { CiYaml: "github-ci.yml", @@ -30,10 +31,11 @@ export const AsIsFiles = { EditorConfig: ".editorconfig.Template", Json: { CollectionItemSerializer: "CollectionItemSerializer.Template.cs", - DateTimeSerializer: "DateTimeSerializer.Template.cs", DateOnlyConverter: "DateOnlyConverter.Template.cs", + DateTimeSerializer: "DateTimeSerializer.Template.cs", EnumConverter: "EnumConverter.Template.cs", EnumSerializer: "EnumSerializer.Template.cs", + JsonAccessAttribute: "JsonAccessAttribute.Template.cs", JsonConfiguration: "JsonConfiguration.Template.cs", OneOfSerializer: "OneOfSerializer.Template.cs", StringEnumSerializer: "StringEnumSerializer.Template.cs" @@ -54,11 +56,12 @@ export const AsIsFiles = { "test/Pagination/StringCursorTest.Template.cs" ], Json: { - OneOfSerializerTests: "test/Json/OneOfSerializerTests.Template.cs", + DateOnlyJsonTests: "test/Json/DateOnlyJsonTests.Template.cs", + DateTimeJsonTests: "test/Json/DateTimeJsonTests.Template.cs", EnumSerializerTests: "test/Json/EnumSerializerTests.Template.cs", + OneOfSerializerTests: "test/Json/OneOfSerializerTests.Template.cs", StringEnumSerializerTests: "test/Json/StringEnumSerializerTests.Template.cs", - DateOnlyJsonTests: "test/Json/DateOnlyJsonTests.Template.cs", - DateTimeJsonTests: "test/Json/DateTimeJsonTests.Template.cs" + JsonAccessAttributeTests: "test/Json/JsonAccessAttributeTests.Template.cs" } } }; diff --git a/generators/csharp/codegen/src/asIs/JsonAccessAttribute.Template.cs b/generators/csharp/codegen/src/asIs/JsonAccessAttribute.Template.cs new file mode 100644 index 00000000000..bbf95676fb2 --- /dev/null +++ b/generators/csharp/codegen/src/asIs/JsonAccessAttribute.Template.cs @@ -0,0 +1,13 @@ +namespace <%= namespace%>; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly +} \ No newline at end of file diff --git a/generators/csharp/codegen/src/asIs/JsonConfiguration.Template.cs b/generators/csharp/codegen/src/asIs/JsonConfiguration.Template.cs index 204a2e12554..f20f3682667 100644 --- a/generators/csharp/codegen/src/asIs/JsonConfiguration.Template.cs +++ b/generators/csharp/codegen/src/asIs/JsonConfiguration.Template.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace <%= namespace%>; @@ -11,15 +12,57 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = { + Converters = + { new DateTimeSerializer(), - #if USE_PORTABLE_DATE_ONLY +#if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), - #endif - new OneOfSerializer() +#endif + new OneOfSerializer(), }, +#if DEBUG WriteIndented = true, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull +#endif + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; @@ -45,4 +88,4 @@ public static T Deserialize(string json) { return JsonSerializer.Deserialize(json, JsonOptions.JsonSerializerOptions)!; } -} \ No newline at end of file +} diff --git a/generators/csharp/codegen/src/asIs/test/Json/JsonAccessAttributeTests.Template.cs b/generators/csharp/codegen/src/asIs/test/Json/JsonAccessAttributeTests.Template.cs new file mode 100644 index 00000000000..293dcc95b52 --- /dev/null +++ b/generators/csharp/codegen/src/asIs/test/Json/JsonAccessAttributeTests.Template.cs @@ -0,0 +1,44 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using <%= namespace%>.Core; + +namespace <%= namespace%>.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} \ No newline at end of file diff --git a/generators/csharp/codegen/src/asIs/test/Json/OneOfSerializerTests.Template.cs b/generators/csharp/codegen/src/asIs/test/Json/OneOfSerializerTests.Template.cs index 5e49856b296..0df46c6896f 100644 --- a/generators/csharp/codegen/src/asIs/test/Json/OneOfSerializerTests.Template.cs +++ b/generators/csharp/codegen/src/asIs/test/Json/OneOfSerializerTests.Template.cs @@ -4,7 +4,7 @@ using OneOf; using <%= namespace%>.Core; -namespace <%= namespace%>.Test.Core; +namespace <%= namespace%>.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/generators/csharp/codegen/src/asIs/test/Json/StringEnumSerializerTests.Template.cs b/generators/csharp/codegen/src/asIs/test/Json/StringEnumSerializerTests.Template.cs index d83b5a62e1c..882e556b244 100644 --- a/generators/csharp/codegen/src/asIs/test/Json/StringEnumSerializerTests.Template.cs +++ b/generators/csharp/codegen/src/asIs/test/Json/StringEnumSerializerTests.Template.cs @@ -3,7 +3,7 @@ using NUnit.Framework; using <%= namespace%>.Core; -namespace <%= namespace%>.Test.Core; +namespace <%= namespace%>.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/generators/csharp/codegen/src/context/AbstractCsharpGeneratorContext.ts b/generators/csharp/codegen/src/context/AbstractCsharpGeneratorContext.ts index 4804f61042c..b488e9779f2 100644 --- a/generators/csharp/codegen/src/context/AbstractCsharpGeneratorContext.ts +++ b/generators/csharp/codegen/src/context/AbstractCsharpGeneratorContext.ts @@ -1,6 +1,7 @@ import { camelCase, upperFirst } from "lodash-es"; import { AbstractGeneratorContext, FernGeneratorExec, GeneratorNotificationService } from "@fern-api/base-generator"; +import { assertNever } from "@fern-api/core-utils"; import { RelativeFilePath, join } from "@fern-api/fs-utils"; import { @@ -8,6 +9,7 @@ import { HttpHeader, IntermediateRepresentation, Name, + ObjectPropertyAccess, PrimitiveType, PrimitiveTypeV1, TypeDeclaration, @@ -22,6 +24,7 @@ import { CONSTANTS_CLASS_NAME, DATETIME_SERIALIZER_CLASS_NAME, ENUM_SERIALIZER_CLASS_NAME, + JSON_ACCESS_ATTRIBUTE_NAME, JSON_UTILS_CLASS_NAME, ONE_OF_SERIALIZER_CLASS_NAME, STRING_ENUM_SERIALIZER_CLASS_NAME @@ -204,6 +207,27 @@ export abstract class AbstractCsharpGeneratorContext< }); } + public createJsonAccessAttribute(propertyAccess: ObjectPropertyAccess): csharp.Annotation { + let argument: string; + switch (propertyAccess) { + case "READ_ONLY": + argument = "JsonAccessType.ReadOnly"; + break; + case "WRITE_ONLY": + argument = "JsonAccessType.WriteOnly"; + break; + default: + assertNever(propertyAccess); + } + return csharp.annotation({ + reference: csharp.classReference({ + namespace: this.getCoreNamespace(), + name: JSON_ACCESS_ATTRIBUTE_NAME + }), + argument + }); + } + public getJsonExceptionClassReference(): csharp.ClassReference { return csharp.classReference({ namespace: "System.Text.Json", diff --git a/generators/csharp/codegen/src/project/CsharpProject.ts b/generators/csharp/codegen/src/project/CsharpProject.ts index 55e47013030..685f467a37d 100644 --- a/generators/csharp/codegen/src/project/CsharpProject.ts +++ b/generators/csharp/codegen/src/project/CsharpProject.ts @@ -649,7 +649,6 @@ ${this.getAdditionalItemGroups().join(`\n${FOUR_SPACES}`)} result.push("README.md"); - this.context.logger.debug(`this.license ${JSON.stringify(this.license)}`); if (this.license) { result.push( this.license._visit({ diff --git a/generators/csharp/model/src/ModelGeneratorContext.ts b/generators/csharp/model/src/ModelGeneratorContext.ts index 8eac057d56e..0afd10c5188 100644 --- a/generators/csharp/model/src/ModelGeneratorContext.ts +++ b/generators/csharp/model/src/ModelGeneratorContext.ts @@ -38,6 +38,7 @@ export class ModelGeneratorContext extends AbstractCsharpGeneratorContext(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/accept-header/src/SeedAccept.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/accept-header/src/SeedAccept.Test/Core/Json/OneOfSerializerTests.cs index acf10ca15b0..cde16e3e7fd 100644 --- a/seed/csharp-model/accept-header/src/SeedAccept.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/accept-header/src/SeedAccept.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedAccept.Core; -namespace SeedAccept.Test.Core; +namespace SeedAccept.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/accept-header/src/SeedAccept/Core/JsonAccessAttribute.cs b/seed/csharp-model/accept-header/src/SeedAccept/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..3167b538d52 --- /dev/null +++ b/seed/csharp-model/accept-header/src/SeedAccept/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedAccept.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/accept-header/src/SeedAccept/Core/JsonConfiguration.cs b/seed/csharp-model/accept-header/src/SeedAccept/Core/JsonConfiguration.cs index b09f476e8a1..7d2b7fb05c1 100644 --- a/seed/csharp-model/accept-header/src/SeedAccept/Core/JsonConfiguration.cs +++ b/seed/csharp-model/accept-header/src/SeedAccept/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedAccept.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/alias-extends/src/SeedAliasExtends.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/alias-extends/src/SeedAliasExtends.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..b4a08d66290 --- /dev/null +++ b/seed/csharp-model/alias-extends/src/SeedAliasExtends.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedAliasExtends.Core; + +namespace SeedAliasExtends.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/alias-extends/src/SeedAliasExtends.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/alias-extends/src/SeedAliasExtends.Test/Core/Json/OneOfSerializerTests.cs index 04d29f38cfb..2860f84502e 100644 --- a/seed/csharp-model/alias-extends/src/SeedAliasExtends.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/alias-extends/src/SeedAliasExtends.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedAliasExtends.Core; -namespace SeedAliasExtends.Test.Core; +namespace SeedAliasExtends.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/alias-extends/src/SeedAliasExtends/Core/JsonAccessAttribute.cs b/seed/csharp-model/alias-extends/src/SeedAliasExtends/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..385351fdc76 --- /dev/null +++ b/seed/csharp-model/alias-extends/src/SeedAliasExtends/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedAliasExtends.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/alias-extends/src/SeedAliasExtends/Core/JsonConfiguration.cs b/seed/csharp-model/alias-extends/src/SeedAliasExtends/Core/JsonConfiguration.cs index 5ccb64683a5..b90cd75c729 100644 --- a/seed/csharp-model/alias-extends/src/SeedAliasExtends/Core/JsonConfiguration.cs +++ b/seed/csharp-model/alias-extends/src/SeedAliasExtends/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedAliasExtends.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/alias/src/SeedAlias.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/alias/src/SeedAlias.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..42181e1c4f4 --- /dev/null +++ b/seed/csharp-model/alias/src/SeedAlias.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedAlias.Core; + +namespace SeedAlias.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/alias/src/SeedAlias.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/alias/src/SeedAlias.Test/Core/Json/OneOfSerializerTests.cs index fcd7d67d11a..59b8c0e1480 100644 --- a/seed/csharp-model/alias/src/SeedAlias.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/alias/src/SeedAlias.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedAlias.Core; -namespace SeedAlias.Test.Core; +namespace SeedAlias.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/alias/src/SeedAlias/Core/JsonAccessAttribute.cs b/seed/csharp-model/alias/src/SeedAlias/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..ae0add36537 --- /dev/null +++ b/seed/csharp-model/alias/src/SeedAlias/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedAlias.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/alias/src/SeedAlias/Core/JsonConfiguration.cs b/seed/csharp-model/alias/src/SeedAlias/Core/JsonConfiguration.cs index 999e4ae0fca..c0f18c7ee6a 100644 --- a/seed/csharp-model/alias/src/SeedAlias/Core/JsonConfiguration.cs +++ b/seed/csharp-model/alias/src/SeedAlias/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedAlias.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/any-auth/src/SeedAnyAuth.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/any-auth/src/SeedAnyAuth.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..02eef339123 --- /dev/null +++ b/seed/csharp-model/any-auth/src/SeedAnyAuth.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedAnyAuth.Core; + +namespace SeedAnyAuth.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/any-auth/src/SeedAnyAuth.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/any-auth/src/SeedAnyAuth.Test/Core/Json/OneOfSerializerTests.cs index 8b05433a560..66ed8d3ea64 100644 --- a/seed/csharp-model/any-auth/src/SeedAnyAuth.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/any-auth/src/SeedAnyAuth.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedAnyAuth.Core; -namespace SeedAnyAuth.Test.Core; +namespace SeedAnyAuth.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/any-auth/src/SeedAnyAuth/Core/JsonAccessAttribute.cs b/seed/csharp-model/any-auth/src/SeedAnyAuth/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..a8810fcafb7 --- /dev/null +++ b/seed/csharp-model/any-auth/src/SeedAnyAuth/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedAnyAuth.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/any-auth/src/SeedAnyAuth/Core/JsonConfiguration.cs b/seed/csharp-model/any-auth/src/SeedAnyAuth/Core/JsonConfiguration.cs index c8bf30357e4..854024d9850 100644 --- a/seed/csharp-model/any-auth/src/SeedAnyAuth/Core/JsonConfiguration.cs +++ b/seed/csharp-model/any-auth/src/SeedAnyAuth/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedAnyAuth.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..e0b3275ec19 --- /dev/null +++ b/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApiWideBasePath.Core; + +namespace SeedApiWideBasePath.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/OneOfSerializerTests.cs index 0163db8662a..b4c63efb975 100644 --- a/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApiWideBasePath.Core; -namespace SeedApiWideBasePath.Test.Core; +namespace SeedApiWideBasePath.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath/Core/JsonAccessAttribute.cs b/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..4b0f9164253 --- /dev/null +++ b/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApiWideBasePath.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath/Core/JsonConfiguration.cs b/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath/Core/JsonConfiguration.cs index db5a9e4f201..a266e5e98c8 100644 --- a/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath/Core/JsonConfiguration.cs +++ b/seed/csharp-model/api-wide-base-path/src/SeedApiWideBasePath/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApiWideBasePath.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/audiences/src/SeedAudiences.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/audiences/src/SeedAudiences.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..a2d0f333cda --- /dev/null +++ b/seed/csharp-model/audiences/src/SeedAudiences.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedAudiences.Core; + +namespace SeedAudiences.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/audiences/src/SeedAudiences.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/audiences/src/SeedAudiences.Test/Core/Json/OneOfSerializerTests.cs index 55ae1c236bf..ad0422ab35b 100644 --- a/seed/csharp-model/audiences/src/SeedAudiences.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/audiences/src/SeedAudiences.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedAudiences.Core; -namespace SeedAudiences.Test.Core; +namespace SeedAudiences.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/audiences/src/SeedAudiences/Core/JsonAccessAttribute.cs b/seed/csharp-model/audiences/src/SeedAudiences/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..fb57dd4e94e --- /dev/null +++ b/seed/csharp-model/audiences/src/SeedAudiences/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedAudiences.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/audiences/src/SeedAudiences/Core/JsonConfiguration.cs b/seed/csharp-model/audiences/src/SeedAudiences/Core/JsonConfiguration.cs index 0b553c3197b..faafad2ea92 100644 --- a/seed/csharp-model/audiences/src/SeedAudiences/Core/JsonConfiguration.cs +++ b/seed/csharp-model/audiences/src/SeedAudiences/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedAudiences.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/auth-environment-variables/src/SeedAuthEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/auth-environment-variables/src/SeedAuthEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..6ef392d0472 --- /dev/null +++ b/seed/csharp-model/auth-environment-variables/src/SeedAuthEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedAuthEnvironmentVariables.Core; + +namespace SeedAuthEnvironmentVariables.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/auth-environment-variables/src/SeedAuthEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/auth-environment-variables/src/SeedAuthEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs index 407414d14f5..157bb1d0a09 100644 --- a/seed/csharp-model/auth-environment-variables/src/SeedAuthEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/auth-environment-variables/src/SeedAuthEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedAuthEnvironmentVariables.Core; -namespace SeedAuthEnvironmentVariables.Test.Core; +namespace SeedAuthEnvironmentVariables.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/auth-environment-variables/src/SeedAuthEnvironmentVariables/Core/JsonAccessAttribute.cs b/seed/csharp-model/auth-environment-variables/src/SeedAuthEnvironmentVariables/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..f023a36081c --- /dev/null +++ b/seed/csharp-model/auth-environment-variables/src/SeedAuthEnvironmentVariables/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedAuthEnvironmentVariables.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/auth-environment-variables/src/SeedAuthEnvironmentVariables/Core/JsonConfiguration.cs b/seed/csharp-model/auth-environment-variables/src/SeedAuthEnvironmentVariables/Core/JsonConfiguration.cs index 4d8a8c59303..379f5f48884 100644 --- a/seed/csharp-model/auth-environment-variables/src/SeedAuthEnvironmentVariables/Core/JsonConfiguration.cs +++ b/seed/csharp-model/auth-environment-variables/src/SeedAuthEnvironmentVariables/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedAuthEnvironmentVariables.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..942585ce4a9 --- /dev/null +++ b/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedBasicAuthEnvironmentVariables.Core; + +namespace SeedBasicAuthEnvironmentVariables.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs index 760e02c3d2b..f0cb6e8903c 100644 --- a/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedBasicAuthEnvironmentVariables.Core; -namespace SeedBasicAuthEnvironmentVariables.Test.Core; +namespace SeedBasicAuthEnvironmentVariables.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/JsonAccessAttribute.cs b/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..8957f27d468 --- /dev/null +++ b/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedBasicAuthEnvironmentVariables.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/JsonConfiguration.cs b/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/JsonConfiguration.cs index 69e95ee717c..e939bc5269a 100644 --- a/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/JsonConfiguration.cs +++ b/seed/csharp-model/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedBasicAuthEnvironmentVariables.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/basic-auth/src/SeedBasicAuth.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/basic-auth/src/SeedBasicAuth.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4969ea2bb2f --- /dev/null +++ b/seed/csharp-model/basic-auth/src/SeedBasicAuth.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedBasicAuth.Core; + +namespace SeedBasicAuth.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/basic-auth/src/SeedBasicAuth.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/basic-auth/src/SeedBasicAuth.Test/Core/Json/OneOfSerializerTests.cs index 5227783cb6e..c164dfefa8b 100644 --- a/seed/csharp-model/basic-auth/src/SeedBasicAuth.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/basic-auth/src/SeedBasicAuth.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedBasicAuth.Core; -namespace SeedBasicAuth.Test.Core; +namespace SeedBasicAuth.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/basic-auth/src/SeedBasicAuth/Core/JsonAccessAttribute.cs b/seed/csharp-model/basic-auth/src/SeedBasicAuth/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..43d96615d6a --- /dev/null +++ b/seed/csharp-model/basic-auth/src/SeedBasicAuth/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedBasicAuth.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/basic-auth/src/SeedBasicAuth/Core/JsonConfiguration.cs b/seed/csharp-model/basic-auth/src/SeedBasicAuth/Core/JsonConfiguration.cs index 2f8ddc7fc5a..8623bea49bf 100644 --- a/seed/csharp-model/basic-auth/src/SeedBasicAuth/Core/JsonConfiguration.cs +++ b/seed/csharp-model/basic-auth/src/SeedBasicAuth/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedBasicAuth.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..5c44c6c5acf --- /dev/null +++ b/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedBearerTokenEnvironmentVariable.Core; + +namespace SeedBearerTokenEnvironmentVariable.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/OneOfSerializerTests.cs index 1e7397c451b..3c0a59cb056 100644 --- a/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedBearerTokenEnvironmentVariable.Core; -namespace SeedBearerTokenEnvironmentVariable.Test.Core; +namespace SeedBearerTokenEnvironmentVariable.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/JsonAccessAttribute.cs b/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..a9ce3382abb --- /dev/null +++ b/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedBearerTokenEnvironmentVariable.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/JsonConfiguration.cs b/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/JsonConfiguration.cs index 67af9cbaefa..65364350785 100644 --- a/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/JsonConfiguration.cs +++ b/seed/csharp-model/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedBearerTokenEnvironmentVariable.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/bytes/src/SeedBytes.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/bytes/src/SeedBytes.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..d7641221172 --- /dev/null +++ b/seed/csharp-model/bytes/src/SeedBytes.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedBytes.Core; + +namespace SeedBytes.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/bytes/src/SeedBytes.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/bytes/src/SeedBytes.Test/Core/Json/OneOfSerializerTests.cs index e5167d3e349..8c8825a375d 100644 --- a/seed/csharp-model/bytes/src/SeedBytes.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/bytes/src/SeedBytes.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedBytes.Core; -namespace SeedBytes.Test.Core; +namespace SeedBytes.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/bytes/src/SeedBytes/Core/JsonAccessAttribute.cs b/seed/csharp-model/bytes/src/SeedBytes/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..72922a6eb25 --- /dev/null +++ b/seed/csharp-model/bytes/src/SeedBytes/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedBytes.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/bytes/src/SeedBytes/Core/JsonConfiguration.cs b/seed/csharp-model/bytes/src/SeedBytes/Core/JsonConfiguration.cs index 4a1af27ba51..2ac6f894de7 100644 --- a/seed/csharp-model/bytes/src/SeedBytes/Core/JsonConfiguration.cs +++ b/seed/csharp-model/bytes/src/SeedBytes/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedBytes.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/circular-references-advanced/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/circular-references-advanced/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-model/circular-references-advanced/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/circular-references-advanced/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/circular-references-advanced/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-model/circular-references-advanced/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/circular-references-advanced/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/circular-references-advanced/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-model/circular-references-advanced/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-model/circular-references-advanced/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/circular-references-advanced/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-model/circular-references-advanced/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-model/circular-references-advanced/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-model/circular-references-advanced/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/circular-references/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/circular-references/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-model/circular-references/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/circular-references/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/circular-references/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-model/circular-references/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/circular-references/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/circular-references/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-model/circular-references/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-model/circular-references/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/circular-references/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-model/circular-references/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-model/circular-references/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-model/circular-references/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..604c6210cd2 --- /dev/null +++ b/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedCrossPackageTypeNames.Core; + +namespace SeedCrossPackageTypeNames.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/OneOfSerializerTests.cs index d27f091f2ef..86df1df8c4d 100644 --- a/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedCrossPackageTypeNames.Core; -namespace SeedCrossPackageTypeNames.Test.Core; +namespace SeedCrossPackageTypeNames.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/JsonAccessAttribute.cs b/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..5680b7ab916 --- /dev/null +++ b/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedCrossPackageTypeNames.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/JsonConfiguration.cs b/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/JsonConfiguration.cs index f017fc39fdf..ad49b4596c6 100644 --- a/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/JsonConfiguration.cs +++ b/seed/csharp-model/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedCrossPackageTypeNames.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-model/csharp-grpc-proto-exhaustive/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/csharp-grpc-proto/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/csharp-grpc-proto/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/csharp-grpc-proto/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/csharp-grpc-proto/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-model/csharp-grpc-proto/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/csharp-grpc-proto/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/csharp-grpc-proto/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-model/csharp-grpc-proto/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-model/csharp-grpc-proto/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/csharp-grpc-proto/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-model/csharp-grpc-proto/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-model/csharp-grpc-proto/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-model/csharp-grpc-proto/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..34a3f9d931c --- /dev/null +++ b/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedCsharpNamespaceConflict.Core; + +namespace SeedCsharpNamespaceConflict.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/OneOfSerializerTests.cs index 509709e0e1e..b7c9794e33f 100644 --- a/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedCsharpNamespaceConflict.Core; -namespace SeedCsharpNamespaceConflict.Test.Core; +namespace SeedCsharpNamespaceConflict.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/JsonAccessAttribute.cs b/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..580008a441b --- /dev/null +++ b/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedCsharpNamespaceConflict.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/JsonConfiguration.cs b/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/JsonConfiguration.cs index 8d8fb16d524..927f59dd311 100644 --- a/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/JsonConfiguration.cs +++ b/seed/csharp-model/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedCsharpNamespaceConflict.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/csharp-property-access/src/SeedCsharpAccess.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/csharp-property-access/src/SeedCsharpAccess.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..e3000217eaf --- /dev/null +++ b/seed/csharp-model/csharp-property-access/src/SeedCsharpAccess.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedCsharpAccess.Core; + +namespace SeedCsharpAccess.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/csharp-property-access/src/SeedCsharpAccess.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/csharp-property-access/src/SeedCsharpAccess.Test/Core/Json/OneOfSerializerTests.cs index 27ed85cb52a..e4426e90477 100644 --- a/seed/csharp-model/csharp-property-access/src/SeedCsharpAccess.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/csharp-property-access/src/SeedCsharpAccess.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedCsharpAccess.Core; -namespace SeedCsharpAccess.Test.Core; +namespace SeedCsharpAccess.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/csharp-property-access/src/SeedCsharpAccess/Core/JsonAccessAttribute.cs b/seed/csharp-model/csharp-property-access/src/SeedCsharpAccess/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..2509c3f0e56 --- /dev/null +++ b/seed/csharp-model/csharp-property-access/src/SeedCsharpAccess/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedCsharpAccess.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/csharp-property-access/src/SeedCsharpAccess/Core/JsonConfiguration.cs b/seed/csharp-model/csharp-property-access/src/SeedCsharpAccess/Core/JsonConfiguration.cs index be18948464e..6a38051079e 100644 --- a/seed/csharp-model/csharp-property-access/src/SeedCsharpAccess/Core/JsonConfiguration.cs +++ b/seed/csharp-model/csharp-property-access/src/SeedCsharpAccess/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedCsharpAccess.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/custom-auth/src/SeedCustomAuth.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/custom-auth/src/SeedCustomAuth.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..5e447369605 --- /dev/null +++ b/seed/csharp-model/custom-auth/src/SeedCustomAuth.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedCustomAuth.Core; + +namespace SeedCustomAuth.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/custom-auth/src/SeedCustomAuth.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/custom-auth/src/SeedCustomAuth.Test/Core/Json/OneOfSerializerTests.cs index b923fda7f6f..0678d56cc70 100644 --- a/seed/csharp-model/custom-auth/src/SeedCustomAuth.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/custom-auth/src/SeedCustomAuth.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedCustomAuth.Core; -namespace SeedCustomAuth.Test.Core; +namespace SeedCustomAuth.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/custom-auth/src/SeedCustomAuth/Core/JsonAccessAttribute.cs b/seed/csharp-model/custom-auth/src/SeedCustomAuth/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..36bb7f51d1d --- /dev/null +++ b/seed/csharp-model/custom-auth/src/SeedCustomAuth/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedCustomAuth.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/custom-auth/src/SeedCustomAuth/Core/JsonConfiguration.cs b/seed/csharp-model/custom-auth/src/SeedCustomAuth/Core/JsonConfiguration.cs index 37d828525ea..ed5c1c0fecc 100644 --- a/seed/csharp-model/custom-auth/src/SeedCustomAuth/Core/JsonConfiguration.cs +++ b/seed/csharp-model/custom-auth/src/SeedCustomAuth/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedCustomAuth.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..ff898fffba9 --- /dev/null +++ b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedEnum.Core; + +namespace SeedEnum.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/OneOfSerializerTests.cs index aafbf33d8ee..6db3afe1231 100644 --- a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedEnum.Core; -namespace SeedEnum.Test.Core; +namespace SeedEnum.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/StringEnumSerializerTests.cs index dce7a6b89c1..ca4e6b6433c 100644 --- a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/StringEnumSerializerTests.cs +++ b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/StringEnumSerializerTests.cs @@ -3,7 +3,7 @@ using NUnit.Framework; using SeedEnum.Core; -namespace SeedEnum.Test.Core; +namespace SeedEnum.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Core/JsonAccessAttribute.cs b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..d1e3d5a9e30 --- /dev/null +++ b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedEnum.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Core/JsonConfiguration.cs b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Core/JsonConfiguration.cs index 9093165cfc8..14bad31cc03 100644 --- a/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Core/JsonConfiguration.cs +++ b/seed/csharp-model/enum/forward-compatible-enums/src/SeedEnum/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedEnum.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/enum/plain-enums/src/SeedEnum.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/enum/plain-enums/src/SeedEnum.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..ff898fffba9 --- /dev/null +++ b/seed/csharp-model/enum/plain-enums/src/SeedEnum.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedEnum.Core; + +namespace SeedEnum.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/enum/plain-enums/src/SeedEnum.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/enum/plain-enums/src/SeedEnum.Test/Core/Json/OneOfSerializerTests.cs index aafbf33d8ee..6db3afe1231 100644 --- a/seed/csharp-model/enum/plain-enums/src/SeedEnum.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/enum/plain-enums/src/SeedEnum.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedEnum.Core; -namespace SeedEnum.Test.Core; +namespace SeedEnum.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/enum/plain-enums/src/SeedEnum/Core/JsonAccessAttribute.cs b/seed/csharp-model/enum/plain-enums/src/SeedEnum/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..d1e3d5a9e30 --- /dev/null +++ b/seed/csharp-model/enum/plain-enums/src/SeedEnum/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedEnum.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/enum/plain-enums/src/SeedEnum/Core/JsonConfiguration.cs b/seed/csharp-model/enum/plain-enums/src/SeedEnum/Core/JsonConfiguration.cs index 9093165cfc8..14bad31cc03 100644 --- a/seed/csharp-model/enum/plain-enums/src/SeedEnum/Core/JsonConfiguration.cs +++ b/seed/csharp-model/enum/plain-enums/src/SeedEnum/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedEnum.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/error-property/src/SeedErrorProperty.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/error-property/src/SeedErrorProperty.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..2358e0d58f7 --- /dev/null +++ b/seed/csharp-model/error-property/src/SeedErrorProperty.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedErrorProperty.Core; + +namespace SeedErrorProperty.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/error-property/src/SeedErrorProperty.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/error-property/src/SeedErrorProperty.Test/Core/Json/OneOfSerializerTests.cs index 8cc055b7506..54c97df1e3d 100644 --- a/seed/csharp-model/error-property/src/SeedErrorProperty.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/error-property/src/SeedErrorProperty.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedErrorProperty.Core; -namespace SeedErrorProperty.Test.Core; +namespace SeedErrorProperty.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/error-property/src/SeedErrorProperty/Core/JsonAccessAttribute.cs b/seed/csharp-model/error-property/src/SeedErrorProperty/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..854c2ac17a2 --- /dev/null +++ b/seed/csharp-model/error-property/src/SeedErrorProperty/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedErrorProperty.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/error-property/src/SeedErrorProperty/Core/JsonConfiguration.cs b/seed/csharp-model/error-property/src/SeedErrorProperty/Core/JsonConfiguration.cs index eb552691f93..6d00524206f 100644 --- a/seed/csharp-model/error-property/src/SeedErrorProperty/Core/JsonConfiguration.cs +++ b/seed/csharp-model/error-property/src/SeedErrorProperty/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedErrorProperty.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/examples/src/SeedExamples.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/examples/src/SeedExamples.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..82f60a04cc7 --- /dev/null +++ b/seed/csharp-model/examples/src/SeedExamples.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedExamples.Core; + +namespace SeedExamples.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/examples/src/SeedExamples.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/examples/src/SeedExamples.Test/Core/Json/OneOfSerializerTests.cs index 349367407b7..fd8ff174c6a 100644 --- a/seed/csharp-model/examples/src/SeedExamples.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/examples/src/SeedExamples.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedExamples.Core; -namespace SeedExamples.Test.Core; +namespace SeedExamples.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/examples/src/SeedExamples/Core/JsonAccessAttribute.cs b/seed/csharp-model/examples/src/SeedExamples/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..464eb97e6e8 --- /dev/null +++ b/seed/csharp-model/examples/src/SeedExamples/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedExamples.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/examples/src/SeedExamples/Core/JsonConfiguration.cs b/seed/csharp-model/examples/src/SeedExamples/Core/JsonConfiguration.cs index 6026c51662f..8ea160f067d 100644 --- a/seed/csharp-model/examples/src/SeedExamples/Core/JsonConfiguration.cs +++ b/seed/csharp-model/examples/src/SeedExamples/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedExamples.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/exhaustive/src/SeedExhaustive.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/exhaustive/src/SeedExhaustive.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..ece7d65f4bd --- /dev/null +++ b/seed/csharp-model/exhaustive/src/SeedExhaustive.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedExhaustive.Core; + +namespace SeedExhaustive.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/exhaustive/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/exhaustive/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs index 017f04af495..bdd76df7bd7 100644 --- a/seed/csharp-model/exhaustive/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/exhaustive/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedExhaustive.Core; -namespace SeedExhaustive.Test.Core; +namespace SeedExhaustive.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/exhaustive/src/SeedExhaustive/Core/JsonAccessAttribute.cs b/seed/csharp-model/exhaustive/src/SeedExhaustive/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..02ad17d185e --- /dev/null +++ b/seed/csharp-model/exhaustive/src/SeedExhaustive/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedExhaustive.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/exhaustive/src/SeedExhaustive/Core/JsonConfiguration.cs b/seed/csharp-model/exhaustive/src/SeedExhaustive/Core/JsonConfiguration.cs index 692ecefab2e..acb0182bd9e 100644 --- a/seed/csharp-model/exhaustive/src/SeedExhaustive/Core/JsonConfiguration.cs +++ b/seed/csharp-model/exhaustive/src/SeedExhaustive/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedExhaustive.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/extends/src/SeedExtends.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/extends/src/SeedExtends.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..c25557e8de4 --- /dev/null +++ b/seed/csharp-model/extends/src/SeedExtends.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedExtends.Core; + +namespace SeedExtends.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/extends/src/SeedExtends.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/extends/src/SeedExtends.Test/Core/Json/OneOfSerializerTests.cs index ead4b973fc6..f9124e1be1b 100644 --- a/seed/csharp-model/extends/src/SeedExtends.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/extends/src/SeedExtends.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedExtends.Core; -namespace SeedExtends.Test.Core; +namespace SeedExtends.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/extends/src/SeedExtends/Core/JsonAccessAttribute.cs b/seed/csharp-model/extends/src/SeedExtends/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..2405d9d9f2a --- /dev/null +++ b/seed/csharp-model/extends/src/SeedExtends/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedExtends.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/extends/src/SeedExtends/Core/JsonConfiguration.cs b/seed/csharp-model/extends/src/SeedExtends/Core/JsonConfiguration.cs index 3cd94891cf0..10077901bd0 100644 --- a/seed/csharp-model/extends/src/SeedExtends/Core/JsonConfiguration.cs +++ b/seed/csharp-model/extends/src/SeedExtends/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedExtends.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/extra-properties/src/SeedExtraProperties.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/extra-properties/src/SeedExtraProperties.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4bbf4d07d60 --- /dev/null +++ b/seed/csharp-model/extra-properties/src/SeedExtraProperties.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedExtraProperties.Core; + +namespace SeedExtraProperties.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/extra-properties/src/SeedExtraProperties.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/extra-properties/src/SeedExtraProperties.Test/Core/Json/OneOfSerializerTests.cs index b29400fbcf6..8b4d40f2199 100644 --- a/seed/csharp-model/extra-properties/src/SeedExtraProperties.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/extra-properties/src/SeedExtraProperties.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedExtraProperties.Core; -namespace SeedExtraProperties.Test.Core; +namespace SeedExtraProperties.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/extra-properties/src/SeedExtraProperties/Core/JsonAccessAttribute.cs b/seed/csharp-model/extra-properties/src/SeedExtraProperties/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..26771f31fc4 --- /dev/null +++ b/seed/csharp-model/extra-properties/src/SeedExtraProperties/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedExtraProperties.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/extra-properties/src/SeedExtraProperties/Core/JsonConfiguration.cs b/seed/csharp-model/extra-properties/src/SeedExtraProperties/Core/JsonConfiguration.cs index 244537b1673..ca7478f9668 100644 --- a/seed/csharp-model/extra-properties/src/SeedExtraProperties/Core/JsonConfiguration.cs +++ b/seed/csharp-model/extra-properties/src/SeedExtraProperties/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedExtraProperties.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/file-download/src/SeedFileDownload.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/file-download/src/SeedFileDownload.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..369396913a1 --- /dev/null +++ b/seed/csharp-model/file-download/src/SeedFileDownload.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedFileDownload.Core; + +namespace SeedFileDownload.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/file-download/src/SeedFileDownload.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/file-download/src/SeedFileDownload.Test/Core/Json/OneOfSerializerTests.cs index 90a8ff3fbc8..1a23cd56986 100644 --- a/seed/csharp-model/file-download/src/SeedFileDownload.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/file-download/src/SeedFileDownload.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedFileDownload.Core; -namespace SeedFileDownload.Test.Core; +namespace SeedFileDownload.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/file-download/src/SeedFileDownload/Core/JsonAccessAttribute.cs b/seed/csharp-model/file-download/src/SeedFileDownload/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..a7c1994b250 --- /dev/null +++ b/seed/csharp-model/file-download/src/SeedFileDownload/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedFileDownload.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/file-download/src/SeedFileDownload/Core/JsonConfiguration.cs b/seed/csharp-model/file-download/src/SeedFileDownload/Core/JsonConfiguration.cs index 95f41e0d6d5..5fa37658237 100644 --- a/seed/csharp-model/file-download/src/SeedFileDownload/Core/JsonConfiguration.cs +++ b/seed/csharp-model/file-download/src/SeedFileDownload/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedFileDownload.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/file-upload/src/SeedFileUpload.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/file-upload/src/SeedFileUpload.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..3d8222fcd11 --- /dev/null +++ b/seed/csharp-model/file-upload/src/SeedFileUpload.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedFileUpload.Core; + +namespace SeedFileUpload.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/file-upload/src/SeedFileUpload.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/file-upload/src/SeedFileUpload.Test/Core/Json/OneOfSerializerTests.cs index 86eedc77887..18da19433e4 100644 --- a/seed/csharp-model/file-upload/src/SeedFileUpload.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/file-upload/src/SeedFileUpload.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedFileUpload.Core; -namespace SeedFileUpload.Test.Core; +namespace SeedFileUpload.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/file-upload/src/SeedFileUpload/Core/JsonAccessAttribute.cs b/seed/csharp-model/file-upload/src/SeedFileUpload/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..26dc63fc452 --- /dev/null +++ b/seed/csharp-model/file-upload/src/SeedFileUpload/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedFileUpload.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/file-upload/src/SeedFileUpload/Core/JsonConfiguration.cs b/seed/csharp-model/file-upload/src/SeedFileUpload/Core/JsonConfiguration.cs index 9f929b95ad7..b4fc86543a8 100644 --- a/seed/csharp-model/file-upload/src/SeedFileUpload/Core/JsonConfiguration.cs +++ b/seed/csharp-model/file-upload/src/SeedFileUpload/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedFileUpload.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/folders/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/folders/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-model/folders/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/folders/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/folders/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-model/folders/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/folders/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/folders/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-model/folders/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-model/folders/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/folders/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-model/folders/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-model/folders/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-model/folders/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..7aa6d192dc7 --- /dev/null +++ b/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedIdempotencyHeaders.Core; + +namespace SeedIdempotencyHeaders.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/OneOfSerializerTests.cs index 9140251a04c..a27cf685986 100644 --- a/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedIdempotencyHeaders.Core; -namespace SeedIdempotencyHeaders.Test.Core; +namespace SeedIdempotencyHeaders.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders/Core/JsonAccessAttribute.cs b/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..61c00f4f1c6 --- /dev/null +++ b/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedIdempotencyHeaders.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders/Core/JsonConfiguration.cs b/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders/Core/JsonConfiguration.cs index 7fe25af1141..de6d287388e 100644 --- a/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders/Core/JsonConfiguration.cs +++ b/seed/csharp-model/idempotency-headers/src/SeedIdempotencyHeaders/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedIdempotencyHeaders.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/imdb/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/imdb/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-model/imdb/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/imdb/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/imdb/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-model/imdb/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/imdb/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/imdb/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-model/imdb/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-model/imdb/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/imdb/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-model/imdb/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-model/imdb/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-model/imdb/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/license/src/SeedLicense.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/license/src/SeedLicense.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..d6649dd5a75 --- /dev/null +++ b/seed/csharp-model/license/src/SeedLicense.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedLicense.Core; + +namespace SeedLicense.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/license/src/SeedLicense.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/license/src/SeedLicense.Test/Core/Json/OneOfSerializerTests.cs index 815d2d9d69a..add9e41b451 100644 --- a/seed/csharp-model/license/src/SeedLicense.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/license/src/SeedLicense.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedLicense.Core; -namespace SeedLicense.Test.Core; +namespace SeedLicense.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/license/src/SeedLicense/Core/JsonAccessAttribute.cs b/seed/csharp-model/license/src/SeedLicense/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..345e157fa63 --- /dev/null +++ b/seed/csharp-model/license/src/SeedLicense/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedLicense.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/license/src/SeedLicense/Core/JsonConfiguration.cs b/seed/csharp-model/license/src/SeedLicense/Core/JsonConfiguration.cs index 5814b888a12..dce4ea6dba5 100644 --- a/seed/csharp-model/license/src/SeedLicense/Core/JsonConfiguration.cs +++ b/seed/csharp-model/license/src/SeedLicense/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedLicense.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/literal/src/SeedLiteral.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/literal/src/SeedLiteral.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..29478771efe --- /dev/null +++ b/seed/csharp-model/literal/src/SeedLiteral.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedLiteral.Core; + +namespace SeedLiteral.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/literal/src/SeedLiteral.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/literal/src/SeedLiteral.Test/Core/Json/OneOfSerializerTests.cs index 1b0eda084ad..9bf7b55394c 100644 --- a/seed/csharp-model/literal/src/SeedLiteral.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/literal/src/SeedLiteral.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedLiteral.Core; -namespace SeedLiteral.Test.Core; +namespace SeedLiteral.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/literal/src/SeedLiteral/Core/JsonAccessAttribute.cs b/seed/csharp-model/literal/src/SeedLiteral/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..8aeba181cfd --- /dev/null +++ b/seed/csharp-model/literal/src/SeedLiteral/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedLiteral.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/literal/src/SeedLiteral/Core/JsonConfiguration.cs b/seed/csharp-model/literal/src/SeedLiteral/Core/JsonConfiguration.cs index 9a3579bb6d0..00ee35d6d0d 100644 --- a/seed/csharp-model/literal/src/SeedLiteral/Core/JsonConfiguration.cs +++ b/seed/csharp-model/literal/src/SeedLiteral/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedLiteral.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/mixed-case/src/SeedMixedCase.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/mixed-case/src/SeedMixedCase.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..f5f2b9c89a6 --- /dev/null +++ b/seed/csharp-model/mixed-case/src/SeedMixedCase.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedMixedCase.Core; + +namespace SeedMixedCase.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/mixed-case/src/SeedMixedCase.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/mixed-case/src/SeedMixedCase.Test/Core/Json/OneOfSerializerTests.cs index 2aa081c347d..59960a214dd 100644 --- a/seed/csharp-model/mixed-case/src/SeedMixedCase.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/mixed-case/src/SeedMixedCase.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedMixedCase.Core; -namespace SeedMixedCase.Test.Core; +namespace SeedMixedCase.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/mixed-case/src/SeedMixedCase/Core/JsonAccessAttribute.cs b/seed/csharp-model/mixed-case/src/SeedMixedCase/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..c1ba1037509 --- /dev/null +++ b/seed/csharp-model/mixed-case/src/SeedMixedCase/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedMixedCase.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/mixed-case/src/SeedMixedCase/Core/JsonConfiguration.cs b/seed/csharp-model/mixed-case/src/SeedMixedCase/Core/JsonConfiguration.cs index 20d84c82d50..3d5de8177cf 100644 --- a/seed/csharp-model/mixed-case/src/SeedMixedCase/Core/JsonConfiguration.cs +++ b/seed/csharp-model/mixed-case/src/SeedMixedCase/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedMixedCase.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..23f08965bbe --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedMixedFileDirectory.Core; + +namespace SeedMixedFileDirectory.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/OneOfSerializerTests.cs index 521c9b2f58e..62a025c4228 100644 --- a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedMixedFileDirectory.Core; -namespace SeedMixedFileDirectory.Test.Core; +namespace SeedMixedFileDirectory.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonAccessAttribute.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..430452b369e --- /dev/null +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedMixedFileDirectory.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonConfiguration.cs b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonConfiguration.cs index d9091ad66d9..6485946fc3a 100644 --- a/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonConfiguration.cs +++ b/seed/csharp-model/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedMixedFileDirectory.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..055674eb1ec --- /dev/null +++ b/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedMultiLineDocs.Core; + +namespace SeedMultiLineDocs.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/OneOfSerializerTests.cs index 22f85d9c958..9910362c6f1 100644 --- a/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedMultiLineDocs.Core; -namespace SeedMultiLineDocs.Test.Core; +namespace SeedMultiLineDocs.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs/Core/JsonAccessAttribute.cs b/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..a4a81c4ac3b --- /dev/null +++ b/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedMultiLineDocs.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs/Core/JsonConfiguration.cs b/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs/Core/JsonConfiguration.cs index 3df81ad657a..0103b34eef9 100644 --- a/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs/Core/JsonConfiguration.cs +++ b/seed/csharp-model/multi-line-docs/src/SeedMultiLineDocs/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedMultiLineDocs.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..622b726f27f --- /dev/null +++ b/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedMultiUrlEnvironmentNoDefault.Core; + +namespace SeedMultiUrlEnvironmentNoDefault.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/OneOfSerializerTests.cs index 6153b18e802..1ecb848cc00 100644 --- a/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedMultiUrlEnvironmentNoDefault.Core; -namespace SeedMultiUrlEnvironmentNoDefault.Test.Core; +namespace SeedMultiUrlEnvironmentNoDefault.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/JsonAccessAttribute.cs b/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..6464de4fcda --- /dev/null +++ b/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedMultiUrlEnvironmentNoDefault.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/JsonConfiguration.cs b/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/JsonConfiguration.cs index 9888b97afb0..66b290bf835 100644 --- a/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/JsonConfiguration.cs +++ b/seed/csharp-model/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedMultiUrlEnvironmentNoDefault.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4565e6423b0 --- /dev/null +++ b/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedMultiUrlEnvironment.Core; + +namespace SeedMultiUrlEnvironment.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment.Test/Core/Json/OneOfSerializerTests.cs index c85463186ef..7571ef81797 100644 --- a/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedMultiUrlEnvironment.Core; -namespace SeedMultiUrlEnvironment.Test.Core; +namespace SeedMultiUrlEnvironment.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment/Core/JsonAccessAttribute.cs b/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..fcd408ffd66 --- /dev/null +++ b/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedMultiUrlEnvironment.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment/Core/JsonConfiguration.cs b/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment/Core/JsonConfiguration.cs index c2216014b06..259b4c7d461 100644 --- a/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment/Core/JsonConfiguration.cs +++ b/seed/csharp-model/multi-url-environment/src/SeedMultiUrlEnvironment/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedMultiUrlEnvironment.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/no-environment/src/SeedNoEnvironment.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/no-environment/src/SeedNoEnvironment.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..1165c3acf8a --- /dev/null +++ b/seed/csharp-model/no-environment/src/SeedNoEnvironment.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedNoEnvironment.Core; + +namespace SeedNoEnvironment.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/no-environment/src/SeedNoEnvironment.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/no-environment/src/SeedNoEnvironment.Test/Core/Json/OneOfSerializerTests.cs index 32be30b3719..bcea0d4d336 100644 --- a/seed/csharp-model/no-environment/src/SeedNoEnvironment.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/no-environment/src/SeedNoEnvironment.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedNoEnvironment.Core; -namespace SeedNoEnvironment.Test.Core; +namespace SeedNoEnvironment.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/no-environment/src/SeedNoEnvironment/Core/JsonAccessAttribute.cs b/seed/csharp-model/no-environment/src/SeedNoEnvironment/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..8e0b9edbf91 --- /dev/null +++ b/seed/csharp-model/no-environment/src/SeedNoEnvironment/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedNoEnvironment.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/no-environment/src/SeedNoEnvironment/Core/JsonConfiguration.cs b/seed/csharp-model/no-environment/src/SeedNoEnvironment/Core/JsonConfiguration.cs index 30461fb6180..885aa34948c 100644 --- a/seed/csharp-model/no-environment/src/SeedNoEnvironment/Core/JsonConfiguration.cs +++ b/seed/csharp-model/no-environment/src/SeedNoEnvironment/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedNoEnvironment.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/nullable/src/SeedNullable.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/nullable/src/SeedNullable.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..59035c323e3 --- /dev/null +++ b/seed/csharp-model/nullable/src/SeedNullable.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedNullable.Core; + +namespace SeedNullable.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/nullable/src/SeedNullable.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/nullable/src/SeedNullable.Test/Core/Json/OneOfSerializerTests.cs index 78ab982ad9e..fedafb19896 100644 --- a/seed/csharp-model/nullable/src/SeedNullable.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/nullable/src/SeedNullable.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedNullable.Core; -namespace SeedNullable.Test.Core; +namespace SeedNullable.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/nullable/src/SeedNullable/Core/JsonAccessAttribute.cs b/seed/csharp-model/nullable/src/SeedNullable/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..d76772e61f4 --- /dev/null +++ b/seed/csharp-model/nullable/src/SeedNullable/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedNullable.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/nullable/src/SeedNullable/Core/JsonConfiguration.cs b/seed/csharp-model/nullable/src/SeedNullable/Core/JsonConfiguration.cs index 37fcd7ddc37..f59de074f3c 100644 --- a/seed/csharp-model/nullable/src/SeedNullable/Core/JsonConfiguration.cs +++ b/seed/csharp-model/nullable/src/SeedNullable/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedNullable.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0cf50c210ee --- /dev/null +++ b/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedOauthClientCredentials.Core; + +namespace SeedOauthClientCredentials.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs index d017b6cb954..37d3a4fe9fe 100644 --- a/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedOauthClientCredentials.Core; -namespace SeedOauthClientCredentials.Test.Core; +namespace SeedOauthClientCredentials.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs b/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..9b6d98bcc99 --- /dev/null +++ b/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedOauthClientCredentials.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs b/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs index 2a3ead79263..16c7fb3807b 100644 --- a/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs +++ b/seed/csharp-model/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedOauthClientCredentials.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..a4c236ff230 --- /dev/null +++ b/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedOauthClientCredentialsDefault.Core; + +namespace SeedOauthClientCredentialsDefault.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/OneOfSerializerTests.cs index ad50e2831ca..6e8cc742714 100644 --- a/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedOauthClientCredentialsDefault.Core; -namespace SeedOauthClientCredentialsDefault.Test.Core; +namespace SeedOauthClientCredentialsDefault.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/JsonAccessAttribute.cs b/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..a5c2fb157c3 --- /dev/null +++ b/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedOauthClientCredentialsDefault.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/JsonConfiguration.cs b/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/JsonConfiguration.cs index 454d0774e5c..491b01bf5ff 100644 --- a/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/JsonConfiguration.cs +++ b/seed/csharp-model/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedOauthClientCredentialsDefault.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..f5c33acea6c --- /dev/null +++ b/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedOauthClientCredentialsEnvironmentVariables.Core; + +namespace SeedOauthClientCredentialsEnvironmentVariables.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs index bf25a3192b4..657fdd1e1b4 100644 --- a/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedOauthClientCredentialsEnvironmentVariables.Core; -namespace SeedOauthClientCredentialsEnvironmentVariables.Test.Core; +namespace SeedOauthClientCredentialsEnvironmentVariables.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/JsonAccessAttribute.cs b/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..77d63ba0bf4 --- /dev/null +++ b/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedOauthClientCredentialsEnvironmentVariables.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/JsonConfiguration.cs b/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/JsonConfiguration.cs index af0d95dbf6c..652ca254630 100644 --- a/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/JsonConfiguration.cs +++ b/seed/csharp-model/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedOauthClientCredentialsEnvironmentVariables.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0cf50c210ee --- /dev/null +++ b/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedOauthClientCredentials.Core; + +namespace SeedOauthClientCredentials.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs index d017b6cb954..37d3a4fe9fe 100644 --- a/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedOauthClientCredentials.Core; -namespace SeedOauthClientCredentials.Test.Core; +namespace SeedOauthClientCredentials.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs b/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..9b6d98bcc99 --- /dev/null +++ b/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedOauthClientCredentials.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs b/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs index 2a3ead79263..16c7fb3807b 100644 --- a/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs +++ b/seed/csharp-model/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedOauthClientCredentials.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0cf50c210ee --- /dev/null +++ b/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedOauthClientCredentials.Core; + +namespace SeedOauthClientCredentials.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs index d017b6cb954..37d3a4fe9fe 100644 --- a/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedOauthClientCredentials.Core; -namespace SeedOauthClientCredentials.Test.Core; +namespace SeedOauthClientCredentials.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs b/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..9b6d98bcc99 --- /dev/null +++ b/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedOauthClientCredentials.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs b/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs index 2a3ead79263..16c7fb3807b 100644 --- a/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs +++ b/seed/csharp-model/oauth-client-credentials/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedOauthClientCredentials.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/object/src/SeedObject.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/object/src/SeedObject.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..9b96faaaebe --- /dev/null +++ b/seed/csharp-model/object/src/SeedObject.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedObject.Core; + +namespace SeedObject.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/object/src/SeedObject.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/object/src/SeedObject.Test/Core/Json/OneOfSerializerTests.cs index 96cefe464ce..79b3c6928cf 100644 --- a/seed/csharp-model/object/src/SeedObject.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/object/src/SeedObject.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedObject.Core; -namespace SeedObject.Test.Core; +namespace SeedObject.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/object/src/SeedObject/Core/JsonAccessAttribute.cs b/seed/csharp-model/object/src/SeedObject/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..4450e251d9a --- /dev/null +++ b/seed/csharp-model/object/src/SeedObject/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedObject.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/object/src/SeedObject/Core/JsonConfiguration.cs b/seed/csharp-model/object/src/SeedObject/Core/JsonConfiguration.cs index 0f04bd86c8e..69fb40be432 100644 --- a/seed/csharp-model/object/src/SeedObject/Core/JsonConfiguration.cs +++ b/seed/csharp-model/object/src/SeedObject/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedObject.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0f4da320fff --- /dev/null +++ b/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedObjectsWithImports.Core; + +namespace SeedObjectsWithImports.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/OneOfSerializerTests.cs index dada8b65e4f..5cd1b0634fd 100644 --- a/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedObjectsWithImports.Core; -namespace SeedObjectsWithImports.Test.Core; +namespace SeedObjectsWithImports.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports/Core/JsonAccessAttribute.cs b/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..4e0086e347f --- /dev/null +++ b/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedObjectsWithImports.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports/Core/JsonConfiguration.cs b/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports/Core/JsonConfiguration.cs index b4883222c42..26020a5a41c 100644 --- a/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports/Core/JsonConfiguration.cs +++ b/seed/csharp-model/objects-with-imports/src/SeedObjectsWithImports/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedObjectsWithImports.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/optional/src/SeedObjectsWithImports.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/optional/src/SeedObjectsWithImports.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0f4da320fff --- /dev/null +++ b/seed/csharp-model/optional/src/SeedObjectsWithImports.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedObjectsWithImports.Core; + +namespace SeedObjectsWithImports.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/optional/src/SeedObjectsWithImports.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/optional/src/SeedObjectsWithImports.Test/Core/Json/OneOfSerializerTests.cs index dada8b65e4f..5cd1b0634fd 100644 --- a/seed/csharp-model/optional/src/SeedObjectsWithImports.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/optional/src/SeedObjectsWithImports.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedObjectsWithImports.Core; -namespace SeedObjectsWithImports.Test.Core; +namespace SeedObjectsWithImports.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/optional/src/SeedObjectsWithImports/Core/JsonAccessAttribute.cs b/seed/csharp-model/optional/src/SeedObjectsWithImports/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..4e0086e347f --- /dev/null +++ b/seed/csharp-model/optional/src/SeedObjectsWithImports/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedObjectsWithImports.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/optional/src/SeedObjectsWithImports/Core/JsonConfiguration.cs b/seed/csharp-model/optional/src/SeedObjectsWithImports/Core/JsonConfiguration.cs index b4883222c42..26020a5a41c 100644 --- a/seed/csharp-model/optional/src/SeedObjectsWithImports/Core/JsonConfiguration.cs +++ b/seed/csharp-model/optional/src/SeedObjectsWithImports/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedObjectsWithImports.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/package-yml/src/SeedPackageYml.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/package-yml/src/SeedPackageYml.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..29d74857346 --- /dev/null +++ b/seed/csharp-model/package-yml/src/SeedPackageYml.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedPackageYml.Core; + +namespace SeedPackageYml.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/package-yml/src/SeedPackageYml.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/package-yml/src/SeedPackageYml.Test/Core/Json/OneOfSerializerTests.cs index d6571509bb9..5c26e1fe155 100644 --- a/seed/csharp-model/package-yml/src/SeedPackageYml.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/package-yml/src/SeedPackageYml.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedPackageYml.Core; -namespace SeedPackageYml.Test.Core; +namespace SeedPackageYml.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/package-yml/src/SeedPackageYml/Core/JsonAccessAttribute.cs b/seed/csharp-model/package-yml/src/SeedPackageYml/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..56dfdd73a81 --- /dev/null +++ b/seed/csharp-model/package-yml/src/SeedPackageYml/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedPackageYml.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/package-yml/src/SeedPackageYml/Core/JsonConfiguration.cs b/seed/csharp-model/package-yml/src/SeedPackageYml/Core/JsonConfiguration.cs index 3268e0ddf28..1624d55d785 100644 --- a/seed/csharp-model/package-yml/src/SeedPackageYml/Core/JsonConfiguration.cs +++ b/seed/csharp-model/package-yml/src/SeedPackageYml/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedPackageYml.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/pagination/src/SeedPagination.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/pagination/src/SeedPagination.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..3effd837aef --- /dev/null +++ b/seed/csharp-model/pagination/src/SeedPagination.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedPagination.Core; + +namespace SeedPagination.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/pagination/src/SeedPagination.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/pagination/src/SeedPagination.Test/Core/Json/OneOfSerializerTests.cs index b5db08fc672..73ed9c358da 100644 --- a/seed/csharp-model/pagination/src/SeedPagination.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/pagination/src/SeedPagination.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedPagination.Core; -namespace SeedPagination.Test.Core; +namespace SeedPagination.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/pagination/src/SeedPagination/Core/JsonAccessAttribute.cs b/seed/csharp-model/pagination/src/SeedPagination/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..dc04c82db59 --- /dev/null +++ b/seed/csharp-model/pagination/src/SeedPagination/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedPagination.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/pagination/src/SeedPagination/Core/JsonConfiguration.cs b/seed/csharp-model/pagination/src/SeedPagination/Core/JsonConfiguration.cs index 2d8199ab77d..49a7820d8eb 100644 --- a/seed/csharp-model/pagination/src/SeedPagination/Core/JsonConfiguration.cs +++ b/seed/csharp-model/pagination/src/SeedPagination/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedPagination.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/path-parameters/src/SeedPathParameters.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/path-parameters/src/SeedPathParameters.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..a19345580ce --- /dev/null +++ b/seed/csharp-model/path-parameters/src/SeedPathParameters.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedPathParameters.Core; + +namespace SeedPathParameters.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/path-parameters/src/SeedPathParameters.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/path-parameters/src/SeedPathParameters.Test/Core/Json/OneOfSerializerTests.cs index 0da45fefa77..ffc7b558b37 100644 --- a/seed/csharp-model/path-parameters/src/SeedPathParameters.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/path-parameters/src/SeedPathParameters.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedPathParameters.Core; -namespace SeedPathParameters.Test.Core; +namespace SeedPathParameters.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/path-parameters/src/SeedPathParameters/Core/JsonAccessAttribute.cs b/seed/csharp-model/path-parameters/src/SeedPathParameters/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..5b0d74eb2cd --- /dev/null +++ b/seed/csharp-model/path-parameters/src/SeedPathParameters/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedPathParameters.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/path-parameters/src/SeedPathParameters/Core/JsonConfiguration.cs b/seed/csharp-model/path-parameters/src/SeedPathParameters/Core/JsonConfiguration.cs index 4092c69538f..6d1b3259c88 100644 --- a/seed/csharp-model/path-parameters/src/SeedPathParameters/Core/JsonConfiguration.cs +++ b/seed/csharp-model/path-parameters/src/SeedPathParameters/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedPathParameters.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/plain-text/src/SeedPlainText.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/plain-text/src/SeedPlainText.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..1029f26d5df --- /dev/null +++ b/seed/csharp-model/plain-text/src/SeedPlainText.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedPlainText.Core; + +namespace SeedPlainText.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/plain-text/src/SeedPlainText.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/plain-text/src/SeedPlainText.Test/Core/Json/OneOfSerializerTests.cs index 577e4830e3b..6a7b41423ed 100644 --- a/seed/csharp-model/plain-text/src/SeedPlainText.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/plain-text/src/SeedPlainText.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedPlainText.Core; -namespace SeedPlainText.Test.Core; +namespace SeedPlainText.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/plain-text/src/SeedPlainText/Core/JsonAccessAttribute.cs b/seed/csharp-model/plain-text/src/SeedPlainText/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..7c3015f8ee6 --- /dev/null +++ b/seed/csharp-model/plain-text/src/SeedPlainText/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedPlainText.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/plain-text/src/SeedPlainText/Core/JsonConfiguration.cs b/seed/csharp-model/plain-text/src/SeedPlainText/Core/JsonConfiguration.cs index 7724bfc9129..46f551bdad6 100644 --- a/seed/csharp-model/plain-text/src/SeedPlainText/Core/JsonConfiguration.cs +++ b/seed/csharp-model/plain-text/src/SeedPlainText/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedPlainText.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/query-parameters/src/SeedQueryParameters.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/query-parameters/src/SeedQueryParameters.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..e0762913a6f --- /dev/null +++ b/seed/csharp-model/query-parameters/src/SeedQueryParameters.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedQueryParameters.Core; + +namespace SeedQueryParameters.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/query-parameters/src/SeedQueryParameters.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/query-parameters/src/SeedQueryParameters.Test/Core/Json/OneOfSerializerTests.cs index 800af10b40e..1fe97cd8cb2 100644 --- a/seed/csharp-model/query-parameters/src/SeedQueryParameters.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/query-parameters/src/SeedQueryParameters.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedQueryParameters.Core; -namespace SeedQueryParameters.Test.Core; +namespace SeedQueryParameters.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/query-parameters/src/SeedQueryParameters/Core/JsonAccessAttribute.cs b/seed/csharp-model/query-parameters/src/SeedQueryParameters/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..60af30593d6 --- /dev/null +++ b/seed/csharp-model/query-parameters/src/SeedQueryParameters/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedQueryParameters.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/query-parameters/src/SeedQueryParameters/Core/JsonConfiguration.cs b/seed/csharp-model/query-parameters/src/SeedQueryParameters/Core/JsonConfiguration.cs index dc6ce48b4ed..9d6f553a648 100644 --- a/seed/csharp-model/query-parameters/src/SeedQueryParameters/Core/JsonConfiguration.cs +++ b/seed/csharp-model/query-parameters/src/SeedQueryParameters/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedQueryParameters.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..1af62958dba --- /dev/null +++ b/seed/csharp-model/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedNurseryApi.Core; + +namespace SeedNurseryApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/OneOfSerializerTests.cs index 28be98e8247..e8e5b17a697 100644 --- a/seed/csharp-model/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedNurseryApi.Core; -namespace SeedNurseryApi.Test.Core; +namespace SeedNurseryApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/reserved-keywords/src/SeedNurseryApi/Core/JsonAccessAttribute.cs b/seed/csharp-model/reserved-keywords/src/SeedNurseryApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..61f3c49983e --- /dev/null +++ b/seed/csharp-model/reserved-keywords/src/SeedNurseryApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedNurseryApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/reserved-keywords/src/SeedNurseryApi/Core/JsonConfiguration.cs b/seed/csharp-model/reserved-keywords/src/SeedNurseryApi/Core/JsonConfiguration.cs index 2e19e4affc6..41a5c23e577 100644 --- a/seed/csharp-model/reserved-keywords/src/SeedNurseryApi/Core/JsonConfiguration.cs +++ b/seed/csharp-model/reserved-keywords/src/SeedNurseryApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedNurseryApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/response-property/src/SeedResponseProperty.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/response-property/src/SeedResponseProperty.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..ad2528c25b8 --- /dev/null +++ b/seed/csharp-model/response-property/src/SeedResponseProperty.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedResponseProperty.Core; + +namespace SeedResponseProperty.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/response-property/src/SeedResponseProperty.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/response-property/src/SeedResponseProperty.Test/Core/Json/OneOfSerializerTests.cs index 0d4e2b6c3fe..314d2807835 100644 --- a/seed/csharp-model/response-property/src/SeedResponseProperty.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/response-property/src/SeedResponseProperty.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedResponseProperty.Core; -namespace SeedResponseProperty.Test.Core; +namespace SeedResponseProperty.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/response-property/src/SeedResponseProperty/Core/JsonAccessAttribute.cs b/seed/csharp-model/response-property/src/SeedResponseProperty/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..43c757d462c --- /dev/null +++ b/seed/csharp-model/response-property/src/SeedResponseProperty/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedResponseProperty.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/response-property/src/SeedResponseProperty/Core/JsonConfiguration.cs b/seed/csharp-model/response-property/src/SeedResponseProperty/Core/JsonConfiguration.cs index 328181c7be1..eedec99b5a9 100644 --- a/seed/csharp-model/response-property/src/SeedResponseProperty/Core/JsonConfiguration.cs +++ b/seed/csharp-model/response-property/src/SeedResponseProperty/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedResponseProperty.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..1c50ab0d2be --- /dev/null +++ b/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedServerSentEvents.Core; + +namespace SeedServerSentEvents.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/OneOfSerializerTests.cs index 175add22ab5..9c2b64cf5e0 100644 --- a/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedServerSentEvents.Core; -namespace SeedServerSentEvents.Test.Core; +namespace SeedServerSentEvents.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents/Core/JsonAccessAttribute.cs b/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..3855e4bc8ed --- /dev/null +++ b/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedServerSentEvents.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents/Core/JsonConfiguration.cs b/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents/Core/JsonConfiguration.cs index fea385bf08c..f522fc212cb 100644 --- a/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents/Core/JsonConfiguration.cs +++ b/seed/csharp-model/server-sent-event-examples/src/SeedServerSentEvents/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedServerSentEvents.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..1c50ab0d2be --- /dev/null +++ b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedServerSentEvents.Core; + +namespace SeedServerSentEvents.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/OneOfSerializerTests.cs index 175add22ab5..9c2b64cf5e0 100644 --- a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedServerSentEvents.Core; -namespace SeedServerSentEvents.Test.Core; +namespace SeedServerSentEvents.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/JsonAccessAttribute.cs b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..3855e4bc8ed --- /dev/null +++ b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedServerSentEvents.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/JsonConfiguration.cs b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/JsonConfiguration.cs index fea385bf08c..f522fc212cb 100644 --- a/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/JsonConfiguration.cs +++ b/seed/csharp-model/server-sent-events/src/SeedServerSentEvents/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedServerSentEvents.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/simple-fhir/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/simple-fhir/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-model/simple-fhir/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/simple-fhir/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/simple-fhir/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-model/simple-fhir/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/simple-fhir/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/simple-fhir/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-model/simple-fhir/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-model/simple-fhir/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/simple-fhir/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-model/simple-fhir/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-model/simple-fhir/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-model/simple-fhir/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0e58215e0fa --- /dev/null +++ b/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedSingleUrlEnvironmentDefault.Core; + +namespace SeedSingleUrlEnvironmentDefault.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/OneOfSerializerTests.cs index 1e8c532a119..a7013fa7ec7 100644 --- a/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedSingleUrlEnvironmentDefault.Core; -namespace SeedSingleUrlEnvironmentDefault.Test.Core; +namespace SeedSingleUrlEnvironmentDefault.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/JsonAccessAttribute.cs b/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..3ccee2ca93a --- /dev/null +++ b/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedSingleUrlEnvironmentDefault.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/JsonConfiguration.cs b/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/JsonConfiguration.cs index 1531acaa29d..55f8e377705 100644 --- a/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/JsonConfiguration.cs +++ b/seed/csharp-model/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedSingleUrlEnvironmentDefault.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..44923808fb9 --- /dev/null +++ b/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedSingleUrlEnvironmentNoDefault.Core; + +namespace SeedSingleUrlEnvironmentNoDefault.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/OneOfSerializerTests.cs index 4e9346ba473..18174ec5053 100644 --- a/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedSingleUrlEnvironmentNoDefault.Core; -namespace SeedSingleUrlEnvironmentNoDefault.Test.Core; +namespace SeedSingleUrlEnvironmentNoDefault.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/JsonAccessAttribute.cs b/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..f8896696441 --- /dev/null +++ b/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedSingleUrlEnvironmentNoDefault.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/JsonConfiguration.cs b/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/JsonConfiguration.cs index 06ff508acb2..d857b74eb75 100644 --- a/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/JsonConfiguration.cs +++ b/seed/csharp-model/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedSingleUrlEnvironmentNoDefault.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/streaming-parameter/src/SeedStreaming.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/streaming-parameter/src/SeedStreaming.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0c5144133a3 --- /dev/null +++ b/seed/csharp-model/streaming-parameter/src/SeedStreaming.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedStreaming.Core; + +namespace SeedStreaming.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/streaming-parameter/src/SeedStreaming.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/streaming-parameter/src/SeedStreaming.Test/Core/Json/OneOfSerializerTests.cs index d291cc246c3..6caae06a78c 100644 --- a/seed/csharp-model/streaming-parameter/src/SeedStreaming.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/streaming-parameter/src/SeedStreaming.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedStreaming.Core; -namespace SeedStreaming.Test.Core; +namespace SeedStreaming.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/streaming-parameter/src/SeedStreaming/Core/JsonAccessAttribute.cs b/seed/csharp-model/streaming-parameter/src/SeedStreaming/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..8668423896d --- /dev/null +++ b/seed/csharp-model/streaming-parameter/src/SeedStreaming/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedStreaming.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/streaming-parameter/src/SeedStreaming/Core/JsonConfiguration.cs b/seed/csharp-model/streaming-parameter/src/SeedStreaming/Core/JsonConfiguration.cs index 7f08fa1b680..f7bf5203666 100644 --- a/seed/csharp-model/streaming-parameter/src/SeedStreaming/Core/JsonConfiguration.cs +++ b/seed/csharp-model/streaming-parameter/src/SeedStreaming/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedStreaming.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/streaming/src/SeedStreaming.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/streaming/src/SeedStreaming.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0c5144133a3 --- /dev/null +++ b/seed/csharp-model/streaming/src/SeedStreaming.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedStreaming.Core; + +namespace SeedStreaming.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/streaming/src/SeedStreaming.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/streaming/src/SeedStreaming.Test/Core/Json/OneOfSerializerTests.cs index d291cc246c3..6caae06a78c 100644 --- a/seed/csharp-model/streaming/src/SeedStreaming.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/streaming/src/SeedStreaming.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedStreaming.Core; -namespace SeedStreaming.Test.Core; +namespace SeedStreaming.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/streaming/src/SeedStreaming/Core/JsonAccessAttribute.cs b/seed/csharp-model/streaming/src/SeedStreaming/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..8668423896d --- /dev/null +++ b/seed/csharp-model/streaming/src/SeedStreaming/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedStreaming.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/streaming/src/SeedStreaming/Core/JsonConfiguration.cs b/seed/csharp-model/streaming/src/SeedStreaming/Core/JsonConfiguration.cs index 7f08fa1b680..f7bf5203666 100644 --- a/seed/csharp-model/streaming/src/SeedStreaming/Core/JsonConfiguration.cs +++ b/seed/csharp-model/streaming/src/SeedStreaming/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedStreaming.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/trace/src/SeedTrace.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/trace/src/SeedTrace.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..b1bcc482557 --- /dev/null +++ b/seed/csharp-model/trace/src/SeedTrace.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedTrace.Core; + +namespace SeedTrace.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/trace/src/SeedTrace.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/trace/src/SeedTrace.Test/Core/Json/OneOfSerializerTests.cs index fa9d06a0015..cdd5a02c1a6 100644 --- a/seed/csharp-model/trace/src/SeedTrace.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/trace/src/SeedTrace.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedTrace.Core; -namespace SeedTrace.Test.Core; +namespace SeedTrace.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/trace/src/SeedTrace/Core/JsonAccessAttribute.cs b/seed/csharp-model/trace/src/SeedTrace/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..4b09f52cd94 --- /dev/null +++ b/seed/csharp-model/trace/src/SeedTrace/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedTrace.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/trace/src/SeedTrace/Core/JsonConfiguration.cs b/seed/csharp-model/trace/src/SeedTrace/Core/JsonConfiguration.cs index 7ff8d7b5516..ef78107561d 100644 --- a/seed/csharp-model/trace/src/SeedTrace/Core/JsonConfiguration.cs +++ b/seed/csharp-model/trace/src/SeedTrace/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedTrace.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..23df802f69a --- /dev/null +++ b/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedUndiscriminatedUnions.Core; + +namespace SeedUndiscriminatedUnions.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/OneOfSerializerTests.cs index 6a5b4a945d1..6bc62cb067c 100644 --- a/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedUndiscriminatedUnions.Core; -namespace SeedUndiscriminatedUnions.Test.Core; +namespace SeedUndiscriminatedUnions.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/JsonAccessAttribute.cs b/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..e8eba4aab06 --- /dev/null +++ b/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedUndiscriminatedUnions.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/JsonConfiguration.cs b/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/JsonConfiguration.cs index 0956b6f3c6b..22f0bac1190 100644 --- a/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/JsonConfiguration.cs +++ b/seed/csharp-model/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedUndiscriminatedUnions.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/unions/src/SeedUnions.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/unions/src/SeedUnions.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..34292f4939b --- /dev/null +++ b/seed/csharp-model/unions/src/SeedUnions.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedUnions.Core; + +namespace SeedUnions.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/unions/src/SeedUnions.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/unions/src/SeedUnions.Test/Core/Json/OneOfSerializerTests.cs index 23d016bcc28..d694be2c591 100644 --- a/seed/csharp-model/unions/src/SeedUnions.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/unions/src/SeedUnions.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedUnions.Core; -namespace SeedUnions.Test.Core; +namespace SeedUnions.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/unions/src/SeedUnions/Core/JsonAccessAttribute.cs b/seed/csharp-model/unions/src/SeedUnions/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..49dc47f0adf --- /dev/null +++ b/seed/csharp-model/unions/src/SeedUnions/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedUnions.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/unions/src/SeedUnions/Core/JsonConfiguration.cs b/seed/csharp-model/unions/src/SeedUnions/Core/JsonConfiguration.cs index 820d6a5382e..ee66618a99d 100644 --- a/seed/csharp-model/unions/src/SeedUnions/Core/JsonConfiguration.cs +++ b/seed/csharp-model/unions/src/SeedUnions/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedUnions.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/unknown/src/SeedUnknownAsAny.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/unknown/src/SeedUnknownAsAny.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..c51898d6811 --- /dev/null +++ b/seed/csharp-model/unknown/src/SeedUnknownAsAny.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedUnknownAsAny.Core; + +namespace SeedUnknownAsAny.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/unknown/src/SeedUnknownAsAny.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/unknown/src/SeedUnknownAsAny.Test/Core/Json/OneOfSerializerTests.cs index 30c621c055c..56ffa2bd6f3 100644 --- a/seed/csharp-model/unknown/src/SeedUnknownAsAny.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/unknown/src/SeedUnknownAsAny.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedUnknownAsAny.Core; -namespace SeedUnknownAsAny.Test.Core; +namespace SeedUnknownAsAny.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/unknown/src/SeedUnknownAsAny/Core/JsonAccessAttribute.cs b/seed/csharp-model/unknown/src/SeedUnknownAsAny/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..1a7b4a7dd39 --- /dev/null +++ b/seed/csharp-model/unknown/src/SeedUnknownAsAny/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedUnknownAsAny.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/unknown/src/SeedUnknownAsAny/Core/JsonConfiguration.cs b/seed/csharp-model/unknown/src/SeedUnknownAsAny/Core/JsonConfiguration.cs index bbee50f2cff..77450a4a645 100644 --- a/seed/csharp-model/unknown/src/SeedUnknownAsAny/Core/JsonConfiguration.cs +++ b/seed/csharp-model/unknown/src/SeedUnknownAsAny/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedUnknownAsAny.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/validation/src/SeedValidation.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/validation/src/SeedValidation.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..07c154cd6d5 --- /dev/null +++ b/seed/csharp-model/validation/src/SeedValidation.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedValidation.Core; + +namespace SeedValidation.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/validation/src/SeedValidation.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/validation/src/SeedValidation.Test/Core/Json/OneOfSerializerTests.cs index 58a68219634..53bf8f7c92f 100644 --- a/seed/csharp-model/validation/src/SeedValidation.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/validation/src/SeedValidation.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedValidation.Core; -namespace SeedValidation.Test.Core; +namespace SeedValidation.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/validation/src/SeedValidation/Core/JsonAccessAttribute.cs b/seed/csharp-model/validation/src/SeedValidation/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..90141301121 --- /dev/null +++ b/seed/csharp-model/validation/src/SeedValidation/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedValidation.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/validation/src/SeedValidation/Core/JsonConfiguration.cs b/seed/csharp-model/validation/src/SeedValidation/Core/JsonConfiguration.cs index f59710f89fb..0cfa8d016a8 100644 --- a/seed/csharp-model/validation/src/SeedValidation/Core/JsonConfiguration.cs +++ b/seed/csharp-model/validation/src/SeedValidation/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedValidation.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/variables/src/SeedVariables.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/variables/src/SeedVariables.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..23e809dd72f --- /dev/null +++ b/seed/csharp-model/variables/src/SeedVariables.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedVariables.Core; + +namespace SeedVariables.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/variables/src/SeedVariables.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/variables/src/SeedVariables.Test/Core/Json/OneOfSerializerTests.cs index 3d60c1dd0ce..6131eedd928 100644 --- a/seed/csharp-model/variables/src/SeedVariables.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/variables/src/SeedVariables.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedVariables.Core; -namespace SeedVariables.Test.Core; +namespace SeedVariables.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/variables/src/SeedVariables/Core/JsonAccessAttribute.cs b/seed/csharp-model/variables/src/SeedVariables/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..9cab94ada6f --- /dev/null +++ b/seed/csharp-model/variables/src/SeedVariables/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedVariables.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/variables/src/SeedVariables/Core/JsonConfiguration.cs b/seed/csharp-model/variables/src/SeedVariables/Core/JsonConfiguration.cs index ddf550d0745..2234e340e4a 100644 --- a/seed/csharp-model/variables/src/SeedVariables/Core/JsonConfiguration.cs +++ b/seed/csharp-model/variables/src/SeedVariables/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedVariables.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/version-no-default/src/SeedVersion.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/version-no-default/src/SeedVersion.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..f636c78d519 --- /dev/null +++ b/seed/csharp-model/version-no-default/src/SeedVersion.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedVersion.Core; + +namespace SeedVersion.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/version-no-default/src/SeedVersion.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/version-no-default/src/SeedVersion.Test/Core/Json/OneOfSerializerTests.cs index 8e2b45b2e08..8a90d4e45cf 100644 --- a/seed/csharp-model/version-no-default/src/SeedVersion.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/version-no-default/src/SeedVersion.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedVersion.Core; -namespace SeedVersion.Test.Core; +namespace SeedVersion.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/version-no-default/src/SeedVersion/Core/JsonAccessAttribute.cs b/seed/csharp-model/version-no-default/src/SeedVersion/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..b84bce6b4ea --- /dev/null +++ b/seed/csharp-model/version-no-default/src/SeedVersion/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedVersion.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/version-no-default/src/SeedVersion/Core/JsonConfiguration.cs b/seed/csharp-model/version-no-default/src/SeedVersion/Core/JsonConfiguration.cs index 1ea339729d3..9505fbfe7b1 100644 --- a/seed/csharp-model/version-no-default/src/SeedVersion/Core/JsonConfiguration.cs +++ b/seed/csharp-model/version-no-default/src/SeedVersion/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedVersion.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/version/src/SeedVersion.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/version/src/SeedVersion.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..f636c78d519 --- /dev/null +++ b/seed/csharp-model/version/src/SeedVersion.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedVersion.Core; + +namespace SeedVersion.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/version/src/SeedVersion.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/version/src/SeedVersion.Test/Core/Json/OneOfSerializerTests.cs index 8e2b45b2e08..8a90d4e45cf 100644 --- a/seed/csharp-model/version/src/SeedVersion.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/version/src/SeedVersion.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedVersion.Core; -namespace SeedVersion.Test.Core; +namespace SeedVersion.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/version/src/SeedVersion/Core/JsonAccessAttribute.cs b/seed/csharp-model/version/src/SeedVersion/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..b84bce6b4ea --- /dev/null +++ b/seed/csharp-model/version/src/SeedVersion/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedVersion.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/version/src/SeedVersion/Core/JsonConfiguration.cs b/seed/csharp-model/version/src/SeedVersion/Core/JsonConfiguration.cs index 1ea339729d3..9505fbfe7b1 100644 --- a/seed/csharp-model/version/src/SeedVersion/Core/JsonConfiguration.cs +++ b/seed/csharp-model/version/src/SeedVersion/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedVersion.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-model/websocket/src/SeedWebsocket.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-model/websocket/src/SeedWebsocket.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..a33d485c6e4 --- /dev/null +++ b/seed/csharp-model/websocket/src/SeedWebsocket.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedWebsocket.Core; + +namespace SeedWebsocket.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-model/websocket/src/SeedWebsocket.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-model/websocket/src/SeedWebsocket.Test/Core/Json/OneOfSerializerTests.cs index 2fa7712800e..7493f21beb1 100644 --- a/seed/csharp-model/websocket/src/SeedWebsocket.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-model/websocket/src/SeedWebsocket.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedWebsocket.Core; -namespace SeedWebsocket.Test.Core; +namespace SeedWebsocket.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-model/websocket/src/SeedWebsocket/Core/JsonAccessAttribute.cs b/seed/csharp-model/websocket/src/SeedWebsocket/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..22eb1e6e24e --- /dev/null +++ b/seed/csharp-model/websocket/src/SeedWebsocket/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedWebsocket.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-model/websocket/src/SeedWebsocket/Core/JsonConfiguration.cs b/seed/csharp-model/websocket/src/SeedWebsocket/Core/JsonConfiguration.cs index 6a1965270be..0c400e2f633 100644 --- a/seed/csharp-model/websocket/src/SeedWebsocket/Core/JsonConfiguration.cs +++ b/seed/csharp-model/websocket/src/SeedWebsocket/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedWebsocket.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/accept-header/src/SeedAccept.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/accept-header/src/SeedAccept.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..ba8d3e930c4 --- /dev/null +++ b/seed/csharp-sdk/accept-header/src/SeedAccept.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedAccept.Core; + +namespace SeedAccept.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/accept-header/src/SeedAccept.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/accept-header/src/SeedAccept.Test/Core/Json/OneOfSerializerTests.cs index acf10ca15b0..cde16e3e7fd 100644 --- a/seed/csharp-sdk/accept-header/src/SeedAccept.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/accept-header/src/SeedAccept.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedAccept.Core; -namespace SeedAccept.Test.Core; +namespace SeedAccept.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/accept-header/src/SeedAccept/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/accept-header/src/SeedAccept/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..3167b538d52 --- /dev/null +++ b/seed/csharp-sdk/accept-header/src/SeedAccept/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedAccept.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/accept-header/src/SeedAccept/Core/JsonConfiguration.cs b/seed/csharp-sdk/accept-header/src/SeedAccept/Core/JsonConfiguration.cs index b09f476e8a1..7d2b7fb05c1 100644 --- a/seed/csharp-sdk/accept-header/src/SeedAccept/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/accept-header/src/SeedAccept/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedAccept.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/alias-extends/src/SeedAliasExtends.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/alias-extends/src/SeedAliasExtends.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..b4a08d66290 --- /dev/null +++ b/seed/csharp-sdk/alias-extends/src/SeedAliasExtends.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedAliasExtends.Core; + +namespace SeedAliasExtends.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/alias-extends/src/SeedAliasExtends.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/alias-extends/src/SeedAliasExtends.Test/Core/Json/OneOfSerializerTests.cs index 04d29f38cfb..2860f84502e 100644 --- a/seed/csharp-sdk/alias-extends/src/SeedAliasExtends.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/alias-extends/src/SeedAliasExtends.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedAliasExtends.Core; -namespace SeedAliasExtends.Test.Core; +namespace SeedAliasExtends.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/alias-extends/src/SeedAliasExtends/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/alias-extends/src/SeedAliasExtends/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..385351fdc76 --- /dev/null +++ b/seed/csharp-sdk/alias-extends/src/SeedAliasExtends/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedAliasExtends.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/alias-extends/src/SeedAliasExtends/Core/JsonConfiguration.cs b/seed/csharp-sdk/alias-extends/src/SeedAliasExtends/Core/JsonConfiguration.cs index 5ccb64683a5..b90cd75c729 100644 --- a/seed/csharp-sdk/alias-extends/src/SeedAliasExtends/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/alias-extends/src/SeedAliasExtends/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedAliasExtends.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/alias/src/SeedAlias.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/alias/src/SeedAlias.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..42181e1c4f4 --- /dev/null +++ b/seed/csharp-sdk/alias/src/SeedAlias.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedAlias.Core; + +namespace SeedAlias.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/alias/src/SeedAlias.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/alias/src/SeedAlias.Test/Core/Json/OneOfSerializerTests.cs index fcd7d67d11a..59b8c0e1480 100644 --- a/seed/csharp-sdk/alias/src/SeedAlias.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/alias/src/SeedAlias.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedAlias.Core; -namespace SeedAlias.Test.Core; +namespace SeedAlias.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/alias/src/SeedAlias/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/alias/src/SeedAlias/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..ae0add36537 --- /dev/null +++ b/seed/csharp-sdk/alias/src/SeedAlias/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedAlias.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/alias/src/SeedAlias/Core/JsonConfiguration.cs b/seed/csharp-sdk/alias/src/SeedAlias/Core/JsonConfiguration.cs index 999e4ae0fca..c0f18c7ee6a 100644 --- a/seed/csharp-sdk/alias/src/SeedAlias/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/alias/src/SeedAlias/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedAlias.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/any-auth/src/SeedAnyAuth.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/any-auth/src/SeedAnyAuth.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..02eef339123 --- /dev/null +++ b/seed/csharp-sdk/any-auth/src/SeedAnyAuth.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedAnyAuth.Core; + +namespace SeedAnyAuth.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/any-auth/src/SeedAnyAuth.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/any-auth/src/SeedAnyAuth.Test/Core/Json/OneOfSerializerTests.cs index 8b05433a560..66ed8d3ea64 100644 --- a/seed/csharp-sdk/any-auth/src/SeedAnyAuth.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/any-auth/src/SeedAnyAuth.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedAnyAuth.Core; -namespace SeedAnyAuth.Test.Core; +namespace SeedAnyAuth.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/any-auth/src/SeedAnyAuth/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/any-auth/src/SeedAnyAuth/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..a8810fcafb7 --- /dev/null +++ b/seed/csharp-sdk/any-auth/src/SeedAnyAuth/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedAnyAuth.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/any-auth/src/SeedAnyAuth/Core/JsonConfiguration.cs b/seed/csharp-sdk/any-auth/src/SeedAnyAuth/Core/JsonConfiguration.cs index c8bf30357e4..854024d9850 100644 --- a/seed/csharp-sdk/any-auth/src/SeedAnyAuth/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/any-auth/src/SeedAnyAuth/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedAnyAuth.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..e0b3275ec19 --- /dev/null +++ b/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApiWideBasePath.Core; + +namespace SeedApiWideBasePath.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/OneOfSerializerTests.cs index 0163db8662a..b4c63efb975 100644 --- a/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApiWideBasePath.Core; -namespace SeedApiWideBasePath.Test.Core; +namespace SeedApiWideBasePath.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..4b0f9164253 --- /dev/null +++ b/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApiWideBasePath.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath/Core/JsonConfiguration.cs b/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath/Core/JsonConfiguration.cs index db5a9e4f201..a266e5e98c8 100644 --- a/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/api-wide-base-path/src/SeedApiWideBasePath/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApiWideBasePath.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/audiences/src/SeedAudiences.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/audiences/src/SeedAudiences.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..a2d0f333cda --- /dev/null +++ b/seed/csharp-sdk/audiences/src/SeedAudiences.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedAudiences.Core; + +namespace SeedAudiences.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/audiences/src/SeedAudiences.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/audiences/src/SeedAudiences.Test/Core/Json/OneOfSerializerTests.cs index 55ae1c236bf..ad0422ab35b 100644 --- a/seed/csharp-sdk/audiences/src/SeedAudiences.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/audiences/src/SeedAudiences.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedAudiences.Core; -namespace SeedAudiences.Test.Core; +namespace SeedAudiences.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/audiences/src/SeedAudiences/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/audiences/src/SeedAudiences/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..fb57dd4e94e --- /dev/null +++ b/seed/csharp-sdk/audiences/src/SeedAudiences/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedAudiences.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/audiences/src/SeedAudiences/Core/JsonConfiguration.cs b/seed/csharp-sdk/audiences/src/SeedAudiences/Core/JsonConfiguration.cs index 0b553c3197b..faafad2ea92 100644 --- a/seed/csharp-sdk/audiences/src/SeedAudiences/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/audiences/src/SeedAudiences/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedAudiences.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/auth-environment-variables/src/SeedAuthEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/auth-environment-variables/src/SeedAuthEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..6ef392d0472 --- /dev/null +++ b/seed/csharp-sdk/auth-environment-variables/src/SeedAuthEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedAuthEnvironmentVariables.Core; + +namespace SeedAuthEnvironmentVariables.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/auth-environment-variables/src/SeedAuthEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/auth-environment-variables/src/SeedAuthEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs index 407414d14f5..157bb1d0a09 100644 --- a/seed/csharp-sdk/auth-environment-variables/src/SeedAuthEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/auth-environment-variables/src/SeedAuthEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedAuthEnvironmentVariables.Core; -namespace SeedAuthEnvironmentVariables.Test.Core; +namespace SeedAuthEnvironmentVariables.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/auth-environment-variables/src/SeedAuthEnvironmentVariables/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/auth-environment-variables/src/SeedAuthEnvironmentVariables/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..f023a36081c --- /dev/null +++ b/seed/csharp-sdk/auth-environment-variables/src/SeedAuthEnvironmentVariables/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedAuthEnvironmentVariables.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/auth-environment-variables/src/SeedAuthEnvironmentVariables/Core/JsonConfiguration.cs b/seed/csharp-sdk/auth-environment-variables/src/SeedAuthEnvironmentVariables/Core/JsonConfiguration.cs index 4d8a8c59303..379f5f48884 100644 --- a/seed/csharp-sdk/auth-environment-variables/src/SeedAuthEnvironmentVariables/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/auth-environment-variables/src/SeedAuthEnvironmentVariables/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedAuthEnvironmentVariables.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..942585ce4a9 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedBasicAuthEnvironmentVariables.Core; + +namespace SeedBasicAuthEnvironmentVariables.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs index 760e02c3d2b..f0cb6e8903c 100644 --- a/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedBasicAuthEnvironmentVariables.Core; -namespace SeedBasicAuthEnvironmentVariables.Test.Core; +namespace SeedBasicAuthEnvironmentVariables.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..8957f27d468 --- /dev/null +++ b/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedBasicAuthEnvironmentVariables.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/JsonConfiguration.cs b/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/JsonConfiguration.cs index 69e95ee717c..e939bc5269a 100644 --- a/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/basic-auth-environment-variables/src/SeedBasicAuthEnvironmentVariables/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedBasicAuthEnvironmentVariables.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4969ea2bb2f --- /dev/null +++ b/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedBasicAuth.Core; + +namespace SeedBasicAuth.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/Json/OneOfSerializerTests.cs index 5227783cb6e..c164dfefa8b 100644 --- a/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/basic-auth/src/SeedBasicAuth.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedBasicAuth.Core; -namespace SeedBasicAuth.Test.Core; +namespace SeedBasicAuth.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/basic-auth/src/SeedBasicAuth/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/basic-auth/src/SeedBasicAuth/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..43d96615d6a --- /dev/null +++ b/seed/csharp-sdk/basic-auth/src/SeedBasicAuth/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedBasicAuth.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/basic-auth/src/SeedBasicAuth/Core/JsonConfiguration.cs b/seed/csharp-sdk/basic-auth/src/SeedBasicAuth/Core/JsonConfiguration.cs index 2f8ddc7fc5a..8623bea49bf 100644 --- a/seed/csharp-sdk/basic-auth/src/SeedBasicAuth/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/basic-auth/src/SeedBasicAuth/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedBasicAuth.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..5c44c6c5acf --- /dev/null +++ b/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedBearerTokenEnvironmentVariable.Core; + +namespace SeedBearerTokenEnvironmentVariable.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/OneOfSerializerTests.cs index 1e7397c451b..3c0a59cb056 100644 --- a/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedBearerTokenEnvironmentVariable.Core; -namespace SeedBearerTokenEnvironmentVariable.Test.Core; +namespace SeedBearerTokenEnvironmentVariable.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..a9ce3382abb --- /dev/null +++ b/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedBearerTokenEnvironmentVariable.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/JsonConfiguration.cs b/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/JsonConfiguration.cs index 67af9cbaefa..65364350785 100644 --- a/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/bearer-token-environment-variable/src/SeedBearerTokenEnvironmentVariable/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedBearerTokenEnvironmentVariable.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/bytes/src/SeedBytes.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/bytes/src/SeedBytes.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..d7641221172 --- /dev/null +++ b/seed/csharp-sdk/bytes/src/SeedBytes.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedBytes.Core; + +namespace SeedBytes.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/bytes/src/SeedBytes.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/bytes/src/SeedBytes.Test/Core/Json/OneOfSerializerTests.cs index e5167d3e349..8c8825a375d 100644 --- a/seed/csharp-sdk/bytes/src/SeedBytes.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/bytes/src/SeedBytes.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedBytes.Core; -namespace SeedBytes.Test.Core; +namespace SeedBytes.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/bytes/src/SeedBytes/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/bytes/src/SeedBytes/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..72922a6eb25 --- /dev/null +++ b/seed/csharp-sdk/bytes/src/SeedBytes/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedBytes.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/bytes/src/SeedBytes/Core/JsonConfiguration.cs b/seed/csharp-sdk/bytes/src/SeedBytes/Core/JsonConfiguration.cs index 4a1af27ba51..2ac6f894de7 100644 --- a/seed/csharp-sdk/bytes/src/SeedBytes/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/bytes/src/SeedBytes/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedBytes.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/circular-references-advanced/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/circular-references-advanced/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-sdk/circular-references-advanced/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/circular-references-advanced/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/circular-references-advanced/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-sdk/circular-references-advanced/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/circular-references-advanced/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/circular-references-advanced/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/circular-references-advanced/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-sdk/circular-references-advanced/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/circular-references-advanced/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/circular-references-advanced/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-sdk/circular-references-advanced/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/circular-references-advanced/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/circular-references/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/circular-references/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-sdk/circular-references/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/circular-references/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/circular-references/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-sdk/circular-references/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/circular-references/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/circular-references/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/circular-references/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-sdk/circular-references/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/circular-references/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/circular-references/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-sdk/circular-references/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/circular-references/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..604c6210cd2 --- /dev/null +++ b/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedCrossPackageTypeNames.Core; + +namespace SeedCrossPackageTypeNames.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/OneOfSerializerTests.cs index d27f091f2ef..86df1df8c4d 100644 --- a/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedCrossPackageTypeNames.Core; -namespace SeedCrossPackageTypeNames.Test.Core; +namespace SeedCrossPackageTypeNames.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..5680b7ab916 --- /dev/null +++ b/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedCrossPackageTypeNames.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/JsonConfiguration.cs b/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/JsonConfiguration.cs index f017fc39fdf..ad49b4596c6 100644 --- a/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/cross-package-type-names/src/SeedCrossPackageTypeNames/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedCrossPackageTypeNames.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/include-exception-handler/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/no-custom-config/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/package-id/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/csharp-grpc-proto-exhaustive/read-only-memory/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/csharp-grpc-proto/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..34a3f9d931c --- /dev/null +++ b/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedCsharpNamespaceConflict.Core; + +namespace SeedCsharpNamespaceConflict.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/OneOfSerializerTests.cs index 509709e0e1e..b7c9794e33f 100644 --- a/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedCsharpNamespaceConflict.Core; -namespace SeedCsharpNamespaceConflict.Test.Core; +namespace SeedCsharpNamespaceConflict.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..580008a441b --- /dev/null +++ b/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedCsharpNamespaceConflict.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/JsonConfiguration.cs b/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/JsonConfiguration.cs index 8d8fb16d524..927f59dd311 100644 --- a/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/csharp-namespace-conflict/src/SeedCsharpNamespaceConflict/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedCsharpNamespaceConflict.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..e3000217eaf --- /dev/null +++ b/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedCsharpAccess.Core; + +namespace SeedCsharpAccess.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess.Test/Core/Json/OneOfSerializerTests.cs index 27ed85cb52a..e4426e90477 100644 --- a/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedCsharpAccess.Core; -namespace SeedCsharpAccess.Test.Core; +namespace SeedCsharpAccess.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..2509c3f0e56 --- /dev/null +++ b/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedCsharpAccess.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess/Core/JsonConfiguration.cs b/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess/Core/JsonConfiguration.cs index be18948464e..6a38051079e 100644 --- a/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedCsharpAccess.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess/Types/User.cs b/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess/Types/User.cs index ee4caffccf8..4762bee294d 100644 --- a/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess/Types/User.cs +++ b/seed/csharp-sdk/csharp-property-access/src/SeedCsharpAccess/Types/User.cs @@ -6,6 +6,7 @@ namespace SeedCsharpAccess; public record User { [JsonPropertyName("id")] + [JsonAccess(JsonAccessType.ReadOnly)] public required string Id { get; set; } [JsonPropertyName("name")] @@ -15,6 +16,7 @@ public record User public required string Email { get; set; } [JsonPropertyName("password")] + [JsonAccess(JsonAccessType.WriteOnly)] public required string Password { get; set; } public override string ToString() diff --git a/seed/csharp-sdk/custom-auth/src/SeedCustomAuth.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/custom-auth/src/SeedCustomAuth.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..5e447369605 --- /dev/null +++ b/seed/csharp-sdk/custom-auth/src/SeedCustomAuth.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedCustomAuth.Core; + +namespace SeedCustomAuth.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/custom-auth/src/SeedCustomAuth.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/custom-auth/src/SeedCustomAuth.Test/Core/Json/OneOfSerializerTests.cs index b923fda7f6f..0678d56cc70 100644 --- a/seed/csharp-sdk/custom-auth/src/SeedCustomAuth.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/custom-auth/src/SeedCustomAuth.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedCustomAuth.Core; -namespace SeedCustomAuth.Test.Core; +namespace SeedCustomAuth.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/custom-auth/src/SeedCustomAuth/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/custom-auth/src/SeedCustomAuth/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..36bb7f51d1d --- /dev/null +++ b/seed/csharp-sdk/custom-auth/src/SeedCustomAuth/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedCustomAuth.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/custom-auth/src/SeedCustomAuth/Core/JsonConfiguration.cs b/seed/csharp-sdk/custom-auth/src/SeedCustomAuth/Core/JsonConfiguration.cs index 37d828525ea..ed5c1c0fecc 100644 --- a/seed/csharp-sdk/custom-auth/src/SeedCustomAuth/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/custom-auth/src/SeedCustomAuth/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedCustomAuth.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..ff898fffba9 --- /dev/null +++ b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedEnum.Core; + +namespace SeedEnum.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/OneOfSerializerTests.cs index aafbf33d8ee..6db3afe1231 100644 --- a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedEnum.Core; -namespace SeedEnum.Test.Core; +namespace SeedEnum.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/StringEnumSerializerTests.cs b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/StringEnumSerializerTests.cs index dce7a6b89c1..ca4e6b6433c 100644 --- a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/StringEnumSerializerTests.cs +++ b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum.Test/Core/Json/StringEnumSerializerTests.cs @@ -3,7 +3,7 @@ using NUnit.Framework; using SeedEnum.Core; -namespace SeedEnum.Test.Core; +namespace SeedEnum.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..d1e3d5a9e30 --- /dev/null +++ b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedEnum.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Core/JsonConfiguration.cs b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Core/JsonConfiguration.cs index 9093165cfc8..14bad31cc03 100644 --- a/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/enum/forward-compatible-enums/src/SeedEnum/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedEnum.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..ff898fffba9 --- /dev/null +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedEnum.Core; + +namespace SeedEnum.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum.Test/Core/Json/OneOfSerializerTests.cs index aafbf33d8ee..6db3afe1231 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedEnum.Core; -namespace SeedEnum.Test.Core; +namespace SeedEnum.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..d1e3d5a9e30 --- /dev/null +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedEnum.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Core/JsonConfiguration.cs b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Core/JsonConfiguration.cs index 9093165cfc8..14bad31cc03 100644 --- a/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/enum/plain-enums/src/SeedEnum/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedEnum.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/error-property/src/SeedErrorProperty.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/error-property/src/SeedErrorProperty.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..2358e0d58f7 --- /dev/null +++ b/seed/csharp-sdk/error-property/src/SeedErrorProperty.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedErrorProperty.Core; + +namespace SeedErrorProperty.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/error-property/src/SeedErrorProperty.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/error-property/src/SeedErrorProperty.Test/Core/Json/OneOfSerializerTests.cs index 8cc055b7506..54c97df1e3d 100644 --- a/seed/csharp-sdk/error-property/src/SeedErrorProperty.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/error-property/src/SeedErrorProperty.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedErrorProperty.Core; -namespace SeedErrorProperty.Test.Core; +namespace SeedErrorProperty.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/error-property/src/SeedErrorProperty/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/error-property/src/SeedErrorProperty/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..854c2ac17a2 --- /dev/null +++ b/seed/csharp-sdk/error-property/src/SeedErrorProperty/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedErrorProperty.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/error-property/src/SeedErrorProperty/Core/JsonConfiguration.cs b/seed/csharp-sdk/error-property/src/SeedErrorProperty/Core/JsonConfiguration.cs index eb552691f93..6d00524206f 100644 --- a/seed/csharp-sdk/error-property/src/SeedErrorProperty/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/error-property/src/SeedErrorProperty/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedErrorProperty.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..82f60a04cc7 --- /dev/null +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedExamples.Core; + +namespace SeedExamples.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Core/Json/OneOfSerializerTests.cs index 349367407b7..fd8ff174c6a 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedExamples.Core; -namespace SeedExamples.Test.Core; +namespace SeedExamples.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..464eb97e6e8 --- /dev/null +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedExamples.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Core/JsonConfiguration.cs b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Core/JsonConfiguration.cs index 6026c51662f..8ea160f067d 100644 --- a/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/examples/no-custom-config/src/SeedExamples/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedExamples.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..82f60a04cc7 --- /dev/null +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedExamples.Core; + +namespace SeedExamples.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Core/Json/OneOfSerializerTests.cs index 349367407b7..fd8ff174c6a 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedExamples.Core; -namespace SeedExamples.Test.Core; +namespace SeedExamples.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..464eb97e6e8 --- /dev/null +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedExamples.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Core/JsonConfiguration.cs b/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Core/JsonConfiguration.cs index 6026c51662f..8ea160f067d 100644 --- a/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/examples/readme-config/src/SeedExamples/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedExamples.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..ece7d65f4bd --- /dev/null +++ b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedExhaustive.Core; + +namespace SeedExhaustive.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs index 017f04af495..bdd76df7bd7 100644 --- a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedExhaustive.Core; -namespace SeedExhaustive.Test.Core; +namespace SeedExhaustive.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..02ad17d185e --- /dev/null +++ b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedExhaustive.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Core/JsonConfiguration.cs b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Core/JsonConfiguration.cs index 692ecefab2e..acb0182bd9e 100644 --- a/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/exhaustive/explicit-namespaces/src/SeedExhaustive/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedExhaustive.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..ece7d65f4bd --- /dev/null +++ b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedExhaustive.Core; + +namespace SeedExhaustive.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs index 017f04af495..bdd76df7bd7 100644 --- a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedExhaustive.Core; -namespace SeedExhaustive.Test.Core; +namespace SeedExhaustive.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..02ad17d185e --- /dev/null +++ b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedExhaustive.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Core/JsonConfiguration.cs b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Core/JsonConfiguration.cs index 692ecefab2e..acb0182bd9e 100644 --- a/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/exhaustive/include-exception-handler/src/SeedExhaustive/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedExhaustive.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..ece7d65f4bd --- /dev/null +++ b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedExhaustive.Core; + +namespace SeedExhaustive.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs index 017f04af495..bdd76df7bd7 100644 --- a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedExhaustive.Core; -namespace SeedExhaustive.Test.Core; +namespace SeedExhaustive.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..02ad17d185e --- /dev/null +++ b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedExhaustive.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Core/JsonConfiguration.cs b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Core/JsonConfiguration.cs index 692ecefab2e..acb0182bd9e 100644 --- a/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/exhaustive/no-generate-error-types/src/SeedExhaustive/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedExhaustive.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..ece7d65f4bd --- /dev/null +++ b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedExhaustive.Core; + +namespace SeedExhaustive.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs index 017f04af495..bdd76df7bd7 100644 --- a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedExhaustive.Core; -namespace SeedExhaustive.Test.Core; +namespace SeedExhaustive.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..02ad17d185e --- /dev/null +++ b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedExhaustive.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Core/JsonConfiguration.cs b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Core/JsonConfiguration.cs index 692ecefab2e..acb0182bd9e 100644 --- a/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/exhaustive/no-root-namespace-for-core-classes/src/SeedExhaustive/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedExhaustive.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/extends/src/SeedExtends.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/extends/src/SeedExtends.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..c25557e8de4 --- /dev/null +++ b/seed/csharp-sdk/extends/src/SeedExtends.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedExtends.Core; + +namespace SeedExtends.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/extends/src/SeedExtends.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/extends/src/SeedExtends.Test/Core/Json/OneOfSerializerTests.cs index ead4b973fc6..f9124e1be1b 100644 --- a/seed/csharp-sdk/extends/src/SeedExtends.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/extends/src/SeedExtends.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedExtends.Core; -namespace SeedExtends.Test.Core; +namespace SeedExtends.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/extends/src/SeedExtends/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/extends/src/SeedExtends/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..2405d9d9f2a --- /dev/null +++ b/seed/csharp-sdk/extends/src/SeedExtends/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedExtends.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/extends/src/SeedExtends/Core/JsonConfiguration.cs b/seed/csharp-sdk/extends/src/SeedExtends/Core/JsonConfiguration.cs index 3cd94891cf0..10077901bd0 100644 --- a/seed/csharp-sdk/extends/src/SeedExtends/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/extends/src/SeedExtends/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedExtends.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/extra-properties/src/SeedExtraProperties.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/extra-properties/src/SeedExtraProperties.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4bbf4d07d60 --- /dev/null +++ b/seed/csharp-sdk/extra-properties/src/SeedExtraProperties.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedExtraProperties.Core; + +namespace SeedExtraProperties.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/extra-properties/src/SeedExtraProperties.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/extra-properties/src/SeedExtraProperties.Test/Core/Json/OneOfSerializerTests.cs index b29400fbcf6..8b4d40f2199 100644 --- a/seed/csharp-sdk/extra-properties/src/SeedExtraProperties.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/extra-properties/src/SeedExtraProperties.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedExtraProperties.Core; -namespace SeedExtraProperties.Test.Core; +namespace SeedExtraProperties.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/extra-properties/src/SeedExtraProperties/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/extra-properties/src/SeedExtraProperties/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..26771f31fc4 --- /dev/null +++ b/seed/csharp-sdk/extra-properties/src/SeedExtraProperties/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedExtraProperties.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/extra-properties/src/SeedExtraProperties/Core/JsonConfiguration.cs b/seed/csharp-sdk/extra-properties/src/SeedExtraProperties/Core/JsonConfiguration.cs index 244537b1673..ca7478f9668 100644 --- a/seed/csharp-sdk/extra-properties/src/SeedExtraProperties/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/extra-properties/src/SeedExtraProperties/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedExtraProperties.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/file-download/src/SeedFileDownload.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/file-download/src/SeedFileDownload.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..369396913a1 --- /dev/null +++ b/seed/csharp-sdk/file-download/src/SeedFileDownload.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedFileDownload.Core; + +namespace SeedFileDownload.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/file-download/src/SeedFileDownload.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/file-download/src/SeedFileDownload.Test/Core/Json/OneOfSerializerTests.cs index 90a8ff3fbc8..1a23cd56986 100644 --- a/seed/csharp-sdk/file-download/src/SeedFileDownload.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/file-download/src/SeedFileDownload.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedFileDownload.Core; -namespace SeedFileDownload.Test.Core; +namespace SeedFileDownload.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/file-download/src/SeedFileDownload/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/file-download/src/SeedFileDownload/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..a7c1994b250 --- /dev/null +++ b/seed/csharp-sdk/file-download/src/SeedFileDownload/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedFileDownload.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/file-download/src/SeedFileDownload/Core/JsonConfiguration.cs b/seed/csharp-sdk/file-download/src/SeedFileDownload/Core/JsonConfiguration.cs index 95f41e0d6d5..5fa37658237 100644 --- a/seed/csharp-sdk/file-download/src/SeedFileDownload/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/file-download/src/SeedFileDownload/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedFileDownload.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/file-upload/src/SeedFileUpload.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/file-upload/src/SeedFileUpload.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..3d8222fcd11 --- /dev/null +++ b/seed/csharp-sdk/file-upload/src/SeedFileUpload.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedFileUpload.Core; + +namespace SeedFileUpload.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/file-upload/src/SeedFileUpload.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/file-upload/src/SeedFileUpload.Test/Core/Json/OneOfSerializerTests.cs index 86eedc77887..18da19433e4 100644 --- a/seed/csharp-sdk/file-upload/src/SeedFileUpload.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/file-upload/src/SeedFileUpload.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedFileUpload.Core; -namespace SeedFileUpload.Test.Core; +namespace SeedFileUpload.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/file-upload/src/SeedFileUpload/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/file-upload/src/SeedFileUpload/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..26dc63fc452 --- /dev/null +++ b/seed/csharp-sdk/file-upload/src/SeedFileUpload/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedFileUpload.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/file-upload/src/SeedFileUpload/Core/JsonConfiguration.cs b/seed/csharp-sdk/file-upload/src/SeedFileUpload/Core/JsonConfiguration.cs index 9f929b95ad7..b4fc86543a8 100644 --- a/seed/csharp-sdk/file-upload/src/SeedFileUpload/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/file-upload/src/SeedFileUpload/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedFileUpload.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/folders/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/folders/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-sdk/folders/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/folders/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/folders/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-sdk/folders/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/folders/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/folders/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/folders/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-sdk/folders/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/folders/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/folders/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-sdk/folders/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/folders/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..7aa6d192dc7 --- /dev/null +++ b/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedIdempotencyHeaders.Core; + +namespace SeedIdempotencyHeaders.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/OneOfSerializerTests.cs index 9140251a04c..a27cf685986 100644 --- a/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedIdempotencyHeaders.Core; -namespace SeedIdempotencyHeaders.Test.Core; +namespace SeedIdempotencyHeaders.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..61c00f4f1c6 --- /dev/null +++ b/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedIdempotencyHeaders.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders/Core/JsonConfiguration.cs b/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders/Core/JsonConfiguration.cs index 7fe25af1141..de6d287388e 100644 --- a/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/idempotency-headers/src/SeedIdempotencyHeaders/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedIdempotencyHeaders.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/imdb/exception-class-names/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/imdb/exported-client-class-name/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/imdb/extra-dependencies/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/imdb/include-exception-handler/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/imdb/no-custom-config/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/license/custom-license/src/SeedLicense.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/license/custom-license/src/SeedLicense.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..d6649dd5a75 --- /dev/null +++ b/seed/csharp-sdk/license/custom-license/src/SeedLicense.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedLicense.Core; + +namespace SeedLicense.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/license/custom-license/src/SeedLicense.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/license/custom-license/src/SeedLicense.Test/Core/Json/OneOfSerializerTests.cs index 815d2d9d69a..add9e41b451 100644 --- a/seed/csharp-sdk/license/custom-license/src/SeedLicense.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/license/custom-license/src/SeedLicense.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedLicense.Core; -namespace SeedLicense.Test.Core; +namespace SeedLicense.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/license/custom-license/src/SeedLicense/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/license/custom-license/src/SeedLicense/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..345e157fa63 --- /dev/null +++ b/seed/csharp-sdk/license/custom-license/src/SeedLicense/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedLicense.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/license/custom-license/src/SeedLicense/Core/JsonConfiguration.cs b/seed/csharp-sdk/license/custom-license/src/SeedLicense/Core/JsonConfiguration.cs index 5814b888a12..dce4ea6dba5 100644 --- a/seed/csharp-sdk/license/custom-license/src/SeedLicense/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/license/custom-license/src/SeedLicense/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedLicense.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/license/mit-license/src/SeedLicense.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/license/mit-license/src/SeedLicense.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..d6649dd5a75 --- /dev/null +++ b/seed/csharp-sdk/license/mit-license/src/SeedLicense.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedLicense.Core; + +namespace SeedLicense.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/license/mit-license/src/SeedLicense.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/license/mit-license/src/SeedLicense.Test/Core/Json/OneOfSerializerTests.cs index 815d2d9d69a..add9e41b451 100644 --- a/seed/csharp-sdk/license/mit-license/src/SeedLicense.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/license/mit-license/src/SeedLicense.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedLicense.Core; -namespace SeedLicense.Test.Core; +namespace SeedLicense.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/license/mit-license/src/SeedLicense/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/license/mit-license/src/SeedLicense/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..345e157fa63 --- /dev/null +++ b/seed/csharp-sdk/license/mit-license/src/SeedLicense/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedLicense.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/license/mit-license/src/SeedLicense/Core/JsonConfiguration.cs b/seed/csharp-sdk/license/mit-license/src/SeedLicense/Core/JsonConfiguration.cs index 5814b888a12..dce4ea6dba5 100644 --- a/seed/csharp-sdk/license/mit-license/src/SeedLicense/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/license/mit-license/src/SeedLicense/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedLicense.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/literal/src/SeedLiteral.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/literal/src/SeedLiteral.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..29478771efe --- /dev/null +++ b/seed/csharp-sdk/literal/src/SeedLiteral.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedLiteral.Core; + +namespace SeedLiteral.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/literal/src/SeedLiteral.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/literal/src/SeedLiteral.Test/Core/Json/OneOfSerializerTests.cs index 1b0eda084ad..9bf7b55394c 100644 --- a/seed/csharp-sdk/literal/src/SeedLiteral.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/literal/src/SeedLiteral.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedLiteral.Core; -namespace SeedLiteral.Test.Core; +namespace SeedLiteral.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/literal/src/SeedLiteral/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/literal/src/SeedLiteral/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..8aeba181cfd --- /dev/null +++ b/seed/csharp-sdk/literal/src/SeedLiteral/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedLiteral.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/literal/src/SeedLiteral/Core/JsonConfiguration.cs b/seed/csharp-sdk/literal/src/SeedLiteral/Core/JsonConfiguration.cs index 9a3579bb6d0..00ee35d6d0d 100644 --- a/seed/csharp-sdk/literal/src/SeedLiteral/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/literal/src/SeedLiteral/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedLiteral.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/mixed-case/src/SeedMixedCase.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/mixed-case/src/SeedMixedCase.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..f5f2b9c89a6 --- /dev/null +++ b/seed/csharp-sdk/mixed-case/src/SeedMixedCase.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedMixedCase.Core; + +namespace SeedMixedCase.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/mixed-case/src/SeedMixedCase.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/mixed-case/src/SeedMixedCase.Test/Core/Json/OneOfSerializerTests.cs index 2aa081c347d..59960a214dd 100644 --- a/seed/csharp-sdk/mixed-case/src/SeedMixedCase.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/mixed-case/src/SeedMixedCase.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedMixedCase.Core; -namespace SeedMixedCase.Test.Core; +namespace SeedMixedCase.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/mixed-case/src/SeedMixedCase/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/mixed-case/src/SeedMixedCase/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..c1ba1037509 --- /dev/null +++ b/seed/csharp-sdk/mixed-case/src/SeedMixedCase/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedMixedCase.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/mixed-case/src/SeedMixedCase/Core/JsonConfiguration.cs b/seed/csharp-sdk/mixed-case/src/SeedMixedCase/Core/JsonConfiguration.cs index 20d84c82d50..3d5de8177cf 100644 --- a/seed/csharp-sdk/mixed-case/src/SeedMixedCase/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/mixed-case/src/SeedMixedCase/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedMixedCase.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..23f08965bbe --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedMixedFileDirectory.Core; + +namespace SeedMixedFileDirectory.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/OneOfSerializerTests.cs index 521c9b2f58e..62a025c4228 100644 --- a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedMixedFileDirectory.Core; -namespace SeedMixedFileDirectory.Test.Core; +namespace SeedMixedFileDirectory.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..430452b369e --- /dev/null +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedMixedFileDirectory.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonConfiguration.cs b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonConfiguration.cs index d9091ad66d9..6485946fc3a 100644 --- a/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/mixed-file-directory/src/SeedMixedFileDirectory/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedMixedFileDirectory.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..055674eb1ec --- /dev/null +++ b/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedMultiLineDocs.Core; + +namespace SeedMultiLineDocs.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/OneOfSerializerTests.cs index 22f85d9c958..9910362c6f1 100644 --- a/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedMultiLineDocs.Core; -namespace SeedMultiLineDocs.Test.Core; +namespace SeedMultiLineDocs.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..a4a81c4ac3b --- /dev/null +++ b/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedMultiLineDocs.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/Core/JsonConfiguration.cs b/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/Core/JsonConfiguration.cs index 3df81ad657a..0103b34eef9 100644 --- a/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/multi-line-docs/src/SeedMultiLineDocs/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedMultiLineDocs.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..622b726f27f --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedMultiUrlEnvironmentNoDefault.Core; + +namespace SeedMultiUrlEnvironmentNoDefault.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/OneOfSerializerTests.cs index 6153b18e802..1ecb848cc00 100644 --- a/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedMultiUrlEnvironmentNoDefault.Core; -namespace SeedMultiUrlEnvironmentNoDefault.Test.Core; +namespace SeedMultiUrlEnvironmentNoDefault.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..6464de4fcda --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedMultiUrlEnvironmentNoDefault.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/JsonConfiguration.cs b/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/JsonConfiguration.cs index 9888b97afb0..66b290bf835 100644 --- a/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/multi-url-environment-no-default/src/SeedMultiUrlEnvironmentNoDefault/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedMultiUrlEnvironmentNoDefault.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4565e6423b0 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedMultiUrlEnvironment.Core; + +namespace SeedMultiUrlEnvironment.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment.Test/Core/Json/OneOfSerializerTests.cs index c85463186ef..7571ef81797 100644 --- a/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedMultiUrlEnvironment.Core; -namespace SeedMultiUrlEnvironment.Test.Core; +namespace SeedMultiUrlEnvironment.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..fcd408ffd66 --- /dev/null +++ b/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedMultiUrlEnvironment.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment/Core/JsonConfiguration.cs b/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment/Core/JsonConfiguration.cs index c2216014b06..259b4c7d461 100644 --- a/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/multi-url-environment/no-pascal-case-environments/src/SeedMultiUrlEnvironment/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedMultiUrlEnvironment.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/no-environment/src/SeedNoEnvironment.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/no-environment/src/SeedNoEnvironment.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..1165c3acf8a --- /dev/null +++ b/seed/csharp-sdk/no-environment/src/SeedNoEnvironment.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedNoEnvironment.Core; + +namespace SeedNoEnvironment.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/no-environment/src/SeedNoEnvironment.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/no-environment/src/SeedNoEnvironment.Test/Core/Json/OneOfSerializerTests.cs index 32be30b3719..bcea0d4d336 100644 --- a/seed/csharp-sdk/no-environment/src/SeedNoEnvironment.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/no-environment/src/SeedNoEnvironment.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedNoEnvironment.Core; -namespace SeedNoEnvironment.Test.Core; +namespace SeedNoEnvironment.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/no-environment/src/SeedNoEnvironment/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/no-environment/src/SeedNoEnvironment/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..8e0b9edbf91 --- /dev/null +++ b/seed/csharp-sdk/no-environment/src/SeedNoEnvironment/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedNoEnvironment.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/no-environment/src/SeedNoEnvironment/Core/JsonConfiguration.cs b/seed/csharp-sdk/no-environment/src/SeedNoEnvironment/Core/JsonConfiguration.cs index 30461fb6180..885aa34948c 100644 --- a/seed/csharp-sdk/no-environment/src/SeedNoEnvironment/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/no-environment/src/SeedNoEnvironment/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedNoEnvironment.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/nullable/src/SeedNullable.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/nullable/src/SeedNullable.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..59035c323e3 --- /dev/null +++ b/seed/csharp-sdk/nullable/src/SeedNullable.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedNullable.Core; + +namespace SeedNullable.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/nullable/src/SeedNullable.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/nullable/src/SeedNullable.Test/Core/Json/OneOfSerializerTests.cs index 78ab982ad9e..fedafb19896 100644 --- a/seed/csharp-sdk/nullable/src/SeedNullable.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/nullable/src/SeedNullable.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedNullable.Core; -namespace SeedNullable.Test.Core; +namespace SeedNullable.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/nullable/src/SeedNullable/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/nullable/src/SeedNullable/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..d76772e61f4 --- /dev/null +++ b/seed/csharp-sdk/nullable/src/SeedNullable/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedNullable.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/nullable/src/SeedNullable/Core/JsonConfiguration.cs b/seed/csharp-sdk/nullable/src/SeedNullable/Core/JsonConfiguration.cs index 37fcd7ddc37..f59de074f3c 100644 --- a/seed/csharp-sdk/nullable/src/SeedNullable/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/nullable/src/SeedNullable/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedNullable.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0cf50c210ee --- /dev/null +++ b/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedOauthClientCredentials.Core; + +namespace SeedOauthClientCredentials.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs index d017b6cb954..37d3a4fe9fe 100644 --- a/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedOauthClientCredentials.Core; -namespace SeedOauthClientCredentials.Test.Core; +namespace SeedOauthClientCredentials.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..9b6d98bcc99 --- /dev/null +++ b/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedOauthClientCredentials.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs b/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs index 2a3ead79263..16c7fb3807b 100644 --- a/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/oauth-client-credentials-custom/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedOauthClientCredentials.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..a4c236ff230 --- /dev/null +++ b/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedOauthClientCredentialsDefault.Core; + +namespace SeedOauthClientCredentialsDefault.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/OneOfSerializerTests.cs index ad50e2831ca..6e8cc742714 100644 --- a/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedOauthClientCredentialsDefault.Core; -namespace SeedOauthClientCredentialsDefault.Test.Core; +namespace SeedOauthClientCredentialsDefault.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..a5c2fb157c3 --- /dev/null +++ b/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedOauthClientCredentialsDefault.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/JsonConfiguration.cs b/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/JsonConfiguration.cs index 454d0774e5c..491b01bf5ff 100644 --- a/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/oauth-client-credentials-default/src/SeedOauthClientCredentialsDefault/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedOauthClientCredentialsDefault.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..f5c33acea6c --- /dev/null +++ b/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedOauthClientCredentialsEnvironmentVariables.Core; + +namespace SeedOauthClientCredentialsEnvironmentVariables.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs index bf25a3192b4..657fdd1e1b4 100644 --- a/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedOauthClientCredentialsEnvironmentVariables.Core; -namespace SeedOauthClientCredentialsEnvironmentVariables.Test.Core; +namespace SeedOauthClientCredentialsEnvironmentVariables.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..77d63ba0bf4 --- /dev/null +++ b/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedOauthClientCredentialsEnvironmentVariables.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/JsonConfiguration.cs b/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/JsonConfiguration.cs index af0d95dbf6c..652ca254630 100644 --- a/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/oauth-client-credentials-environment-variables/src/SeedOauthClientCredentialsEnvironmentVariables/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedOauthClientCredentialsEnvironmentVariables.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0cf50c210ee --- /dev/null +++ b/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedOauthClientCredentials.Core; + +namespace SeedOauthClientCredentials.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs index d017b6cb954..37d3a4fe9fe 100644 --- a/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedOauthClientCredentials.Core; -namespace SeedOauthClientCredentials.Test.Core; +namespace SeedOauthClientCredentials.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..9b6d98bcc99 --- /dev/null +++ b/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedOauthClientCredentials.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs b/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs index 2a3ead79263..16c7fb3807b 100644 --- a/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/oauth-client-credentials-nested-root/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedOauthClientCredentials.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0cf50c210ee --- /dev/null +++ b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedOauthClientCredentials.Core; + +namespace SeedOauthClientCredentials.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs index d017b6cb954..37d3a4fe9fe 100644 --- a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedOauthClientCredentials.Core; -namespace SeedOauthClientCredentials.Test.Core; +namespace SeedOauthClientCredentials.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..9b6d98bcc99 --- /dev/null +++ b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedOauthClientCredentials.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs index 2a3ead79263..16c7fb3807b 100644 --- a/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/oauth-client-credentials/include-exception-handler/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedOauthClientCredentials.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0cf50c210ee --- /dev/null +++ b/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedOauthClientCredentials.Core; + +namespace SeedOauthClientCredentials.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs index d017b6cb954..37d3a4fe9fe 100644 --- a/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedOauthClientCredentials.Core; -namespace SeedOauthClientCredentials.Test.Core; +namespace SeedOauthClientCredentials.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..9b6d98bcc99 --- /dev/null +++ b/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedOauthClientCredentials.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs b/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs index 2a3ead79263..16c7fb3807b 100644 --- a/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/oauth-client-credentials/no-custom-config/src/SeedOauthClientCredentials/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedOauthClientCredentials.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/object/src/SeedObject.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/object/src/SeedObject.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..9b96faaaebe --- /dev/null +++ b/seed/csharp-sdk/object/src/SeedObject.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedObject.Core; + +namespace SeedObject.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/object/src/SeedObject.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/object/src/SeedObject.Test/Core/Json/OneOfSerializerTests.cs index 96cefe464ce..79b3c6928cf 100644 --- a/seed/csharp-sdk/object/src/SeedObject.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/object/src/SeedObject.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedObject.Core; -namespace SeedObject.Test.Core; +namespace SeedObject.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/object/src/SeedObject/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/object/src/SeedObject/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..4450e251d9a --- /dev/null +++ b/seed/csharp-sdk/object/src/SeedObject/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedObject.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/object/src/SeedObject/Core/JsonConfiguration.cs b/seed/csharp-sdk/object/src/SeedObject/Core/JsonConfiguration.cs index 0f04bd86c8e..69fb40be432 100644 --- a/seed/csharp-sdk/object/src/SeedObject/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/object/src/SeedObject/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedObject.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0f4da320fff --- /dev/null +++ b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedObjectsWithImports.Core; + +namespace SeedObjectsWithImports.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/OneOfSerializerTests.cs index dada8b65e4f..5cd1b0634fd 100644 --- a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedObjectsWithImports.Core; -namespace SeedObjectsWithImports.Test.Core; +namespace SeedObjectsWithImports.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..4e0086e347f --- /dev/null +++ b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedObjectsWithImports.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/Core/JsonConfiguration.cs b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/Core/JsonConfiguration.cs index b4883222c42..26020a5a41c 100644 --- a/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/objects-with-imports/src/SeedObjectsWithImports/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedObjectsWithImports.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/optional/no-simplify-object-dictionaries/src/SeedObjectsWithImports.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/optional/no-simplify-object-dictionaries/src/SeedObjectsWithImports.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0f4da320fff --- /dev/null +++ b/seed/csharp-sdk/optional/no-simplify-object-dictionaries/src/SeedObjectsWithImports.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedObjectsWithImports.Core; + +namespace SeedObjectsWithImports.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/optional/no-simplify-object-dictionaries/src/SeedObjectsWithImports.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/optional/no-simplify-object-dictionaries/src/SeedObjectsWithImports.Test/Core/Json/OneOfSerializerTests.cs index dada8b65e4f..5cd1b0634fd 100644 --- a/seed/csharp-sdk/optional/no-simplify-object-dictionaries/src/SeedObjectsWithImports.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/optional/no-simplify-object-dictionaries/src/SeedObjectsWithImports.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedObjectsWithImports.Core; -namespace SeedObjectsWithImports.Test.Core; +namespace SeedObjectsWithImports.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/optional/no-simplify-object-dictionaries/src/SeedObjectsWithImports/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/optional/no-simplify-object-dictionaries/src/SeedObjectsWithImports/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..4e0086e347f --- /dev/null +++ b/seed/csharp-sdk/optional/no-simplify-object-dictionaries/src/SeedObjectsWithImports/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedObjectsWithImports.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/optional/no-simplify-object-dictionaries/src/SeedObjectsWithImports/Core/JsonConfiguration.cs b/seed/csharp-sdk/optional/no-simplify-object-dictionaries/src/SeedObjectsWithImports/Core/JsonConfiguration.cs index b4883222c42..26020a5a41c 100644 --- a/seed/csharp-sdk/optional/no-simplify-object-dictionaries/src/SeedObjectsWithImports/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/optional/no-simplify-object-dictionaries/src/SeedObjectsWithImports/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedObjectsWithImports.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/package-yml/src/SeedPackageYml.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/package-yml/src/SeedPackageYml.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..29d74857346 --- /dev/null +++ b/seed/csharp-sdk/package-yml/src/SeedPackageYml.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedPackageYml.Core; + +namespace SeedPackageYml.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/package-yml/src/SeedPackageYml.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/package-yml/src/SeedPackageYml.Test/Core/Json/OneOfSerializerTests.cs index d6571509bb9..5c26e1fe155 100644 --- a/seed/csharp-sdk/package-yml/src/SeedPackageYml.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/package-yml/src/SeedPackageYml.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedPackageYml.Core; -namespace SeedPackageYml.Test.Core; +namespace SeedPackageYml.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/package-yml/src/SeedPackageYml/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/package-yml/src/SeedPackageYml/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..56dfdd73a81 --- /dev/null +++ b/seed/csharp-sdk/package-yml/src/SeedPackageYml/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedPackageYml.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/package-yml/src/SeedPackageYml/Core/JsonConfiguration.cs b/seed/csharp-sdk/package-yml/src/SeedPackageYml/Core/JsonConfiguration.cs index 3268e0ddf28..1624d55d785 100644 --- a/seed/csharp-sdk/package-yml/src/SeedPackageYml/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/package-yml/src/SeedPackageYml/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedPackageYml.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..3effd837aef --- /dev/null +++ b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedPagination.Core; + +namespace SeedPagination.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination.Test/Core/Json/OneOfSerializerTests.cs index b5db08fc672..73ed9c358da 100644 --- a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedPagination.Core; -namespace SeedPagination.Test.Core; +namespace SeedPagination.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..dc04c82db59 --- /dev/null +++ b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedPagination.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/JsonConfiguration.cs b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/JsonConfiguration.cs index 2d8199ab77d..49a7820d8eb 100644 --- a/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/pagination/custom-pager-with-exception-handler/src/SeedPagination/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedPagination.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..3effd837aef --- /dev/null +++ b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedPagination.Core; + +namespace SeedPagination.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination.Test/Core/Json/OneOfSerializerTests.cs index b5db08fc672..73ed9c358da 100644 --- a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedPagination.Core; -namespace SeedPagination.Test.Core; +namespace SeedPagination.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..dc04c82db59 --- /dev/null +++ b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedPagination.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Core/JsonConfiguration.cs b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Core/JsonConfiguration.cs index 2d8199ab77d..49a7820d8eb 100644 --- a/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/pagination/custom-pager/src/SeedPagination/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedPagination.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..3effd837aef --- /dev/null +++ b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedPagination.Core; + +namespace SeedPagination.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination.Test/Core/Json/OneOfSerializerTests.cs index b5db08fc672..73ed9c358da 100644 --- a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedPagination.Core; -namespace SeedPagination.Test.Core; +namespace SeedPagination.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..dc04c82db59 --- /dev/null +++ b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedPagination.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Core/JsonConfiguration.cs b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Core/JsonConfiguration.cs index 2d8199ab77d..49a7820d8eb 100644 --- a/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/pagination/no-custom-config/src/SeedPagination/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedPagination.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/path-parameters/inline-path-parameters/src/SeedPathParameters.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/path-parameters/inline-path-parameters/src/SeedPathParameters.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..a19345580ce --- /dev/null +++ b/seed/csharp-sdk/path-parameters/inline-path-parameters/src/SeedPathParameters.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedPathParameters.Core; + +namespace SeedPathParameters.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/path-parameters/inline-path-parameters/src/SeedPathParameters.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/path-parameters/inline-path-parameters/src/SeedPathParameters.Test/Core/Json/OneOfSerializerTests.cs index 0da45fefa77..ffc7b558b37 100644 --- a/seed/csharp-sdk/path-parameters/inline-path-parameters/src/SeedPathParameters.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/path-parameters/inline-path-parameters/src/SeedPathParameters.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedPathParameters.Core; -namespace SeedPathParameters.Test.Core; +namespace SeedPathParameters.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/path-parameters/inline-path-parameters/src/SeedPathParameters/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/path-parameters/inline-path-parameters/src/SeedPathParameters/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..5b0d74eb2cd --- /dev/null +++ b/seed/csharp-sdk/path-parameters/inline-path-parameters/src/SeedPathParameters/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedPathParameters.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/path-parameters/inline-path-parameters/src/SeedPathParameters/Core/JsonConfiguration.cs b/seed/csharp-sdk/path-parameters/inline-path-parameters/src/SeedPathParameters/Core/JsonConfiguration.cs index 4092c69538f..6d1b3259c88 100644 --- a/seed/csharp-sdk/path-parameters/inline-path-parameters/src/SeedPathParameters/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/path-parameters/inline-path-parameters/src/SeedPathParameters/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedPathParameters.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..a19345580ce --- /dev/null +++ b/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedPathParameters.Core; + +namespace SeedPathParameters.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters.Test/Core/Json/OneOfSerializerTests.cs index 0da45fefa77..ffc7b558b37 100644 --- a/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedPathParameters.Core; -namespace SeedPathParameters.Test.Core; +namespace SeedPathParameters.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..5b0d74eb2cd --- /dev/null +++ b/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedPathParameters.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters/Core/JsonConfiguration.cs b/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters/Core/JsonConfiguration.cs index 4092c69538f..6d1b3259c88 100644 --- a/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/path-parameters/no-custom-config/src/SeedPathParameters/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedPathParameters.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/plain-text/src/SeedPlainText.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/plain-text/src/SeedPlainText.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..1029f26d5df --- /dev/null +++ b/seed/csharp-sdk/plain-text/src/SeedPlainText.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedPlainText.Core; + +namespace SeedPlainText.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/plain-text/src/SeedPlainText.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/plain-text/src/SeedPlainText.Test/Core/Json/OneOfSerializerTests.cs index 577e4830e3b..6a7b41423ed 100644 --- a/seed/csharp-sdk/plain-text/src/SeedPlainText.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/plain-text/src/SeedPlainText.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedPlainText.Core; -namespace SeedPlainText.Test.Core; +namespace SeedPlainText.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/plain-text/src/SeedPlainText/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/plain-text/src/SeedPlainText/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..7c3015f8ee6 --- /dev/null +++ b/seed/csharp-sdk/plain-text/src/SeedPlainText/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedPlainText.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/plain-text/src/SeedPlainText/Core/JsonConfiguration.cs b/seed/csharp-sdk/plain-text/src/SeedPlainText/Core/JsonConfiguration.cs index 7724bfc9129..46f551bdad6 100644 --- a/seed/csharp-sdk/plain-text/src/SeedPlainText/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/plain-text/src/SeedPlainText/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedPlainText.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/query-parameters/src/SeedQueryParameters.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/query-parameters/src/SeedQueryParameters.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..e0762913a6f --- /dev/null +++ b/seed/csharp-sdk/query-parameters/src/SeedQueryParameters.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedQueryParameters.Core; + +namespace SeedQueryParameters.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/query-parameters/src/SeedQueryParameters.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/query-parameters/src/SeedQueryParameters.Test/Core/Json/OneOfSerializerTests.cs index 800af10b40e..1fe97cd8cb2 100644 --- a/seed/csharp-sdk/query-parameters/src/SeedQueryParameters.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/query-parameters/src/SeedQueryParameters.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedQueryParameters.Core; -namespace SeedQueryParameters.Test.Core; +namespace SeedQueryParameters.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/query-parameters/src/SeedQueryParameters/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/query-parameters/src/SeedQueryParameters/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..60af30593d6 --- /dev/null +++ b/seed/csharp-sdk/query-parameters/src/SeedQueryParameters/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedQueryParameters.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/query-parameters/src/SeedQueryParameters/Core/JsonConfiguration.cs b/seed/csharp-sdk/query-parameters/src/SeedQueryParameters/Core/JsonConfiguration.cs index dc6ce48b4ed..9d6f553a648 100644 --- a/seed/csharp-sdk/query-parameters/src/SeedQueryParameters/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/query-parameters/src/SeedQueryParameters/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedQueryParameters.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..1af62958dba --- /dev/null +++ b/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedNurseryApi.Core; + +namespace SeedNurseryApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/OneOfSerializerTests.cs index 28be98e8247..e8e5b17a697 100644 --- a/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedNurseryApi.Core; -namespace SeedNurseryApi.Test.Core; +namespace SeedNurseryApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..61f3c49983e --- /dev/null +++ b/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedNurseryApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi/Core/JsonConfiguration.cs index 2e19e4affc6..41a5c23e577 100644 --- a/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/reserved-keywords/src/SeedNurseryApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedNurseryApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/response-property/src/SeedResponseProperty.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/response-property/src/SeedResponseProperty.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..ad2528c25b8 --- /dev/null +++ b/seed/csharp-sdk/response-property/src/SeedResponseProperty.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedResponseProperty.Core; + +namespace SeedResponseProperty.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/response-property/src/SeedResponseProperty.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/response-property/src/SeedResponseProperty.Test/Core/Json/OneOfSerializerTests.cs index 0d4e2b6c3fe..314d2807835 100644 --- a/seed/csharp-sdk/response-property/src/SeedResponseProperty.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/response-property/src/SeedResponseProperty.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedResponseProperty.Core; -namespace SeedResponseProperty.Test.Core; +namespace SeedResponseProperty.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/response-property/src/SeedResponseProperty/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/response-property/src/SeedResponseProperty/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..43c757d462c --- /dev/null +++ b/seed/csharp-sdk/response-property/src/SeedResponseProperty/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedResponseProperty.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/response-property/src/SeedResponseProperty/Core/JsonConfiguration.cs b/seed/csharp-sdk/response-property/src/SeedResponseProperty/Core/JsonConfiguration.cs index 328181c7be1..eedec99b5a9 100644 --- a/seed/csharp-sdk/response-property/src/SeedResponseProperty/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/response-property/src/SeedResponseProperty/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedResponseProperty.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..1c50ab0d2be --- /dev/null +++ b/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedServerSentEvents.Core; + +namespace SeedServerSentEvents.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/OneOfSerializerTests.cs index 175add22ab5..9c2b64cf5e0 100644 --- a/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedServerSentEvents.Core; -namespace SeedServerSentEvents.Test.Core; +namespace SeedServerSentEvents.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..3855e4bc8ed --- /dev/null +++ b/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedServerSentEvents.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents/Core/JsonConfiguration.cs b/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents/Core/JsonConfiguration.cs index fea385bf08c..f522fc212cb 100644 --- a/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/server-sent-event-examples/src/SeedServerSentEvents/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedServerSentEvents.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..1c50ab0d2be --- /dev/null +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedServerSentEvents.Core; + +namespace SeedServerSentEvents.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/OneOfSerializerTests.cs index 175add22ab5..9c2b64cf5e0 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedServerSentEvents.Core; -namespace SeedServerSentEvents.Test.Core; +namespace SeedServerSentEvents.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..3855e4bc8ed --- /dev/null +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedServerSentEvents.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/JsonConfiguration.cs b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/JsonConfiguration.cs index fea385bf08c..f522fc212cb 100644 --- a/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/server-sent-events/src/SeedServerSentEvents/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedServerSentEvents.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/simple-fhir/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/simple-fhir/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..4962b2df6d0 --- /dev/null +++ b/seed/csharp-sdk/simple-fhir/src/SeedApi.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedApi.Core; + +namespace SeedApi.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/simple-fhir/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/simple-fhir/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs index 97151d070e4..8843979ae97 100644 --- a/seed/csharp-sdk/simple-fhir/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/simple-fhir/src/SeedApi.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedApi.Core; -namespace SeedApi.Test.Core; +namespace SeedApi.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/simple-fhir/src/SeedApi/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/simple-fhir/src/SeedApi/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..bd2d7e702a3 --- /dev/null +++ b/seed/csharp-sdk/simple-fhir/src/SeedApi/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedApi.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/simple-fhir/src/SeedApi/Core/JsonConfiguration.cs b/seed/csharp-sdk/simple-fhir/src/SeedApi/Core/JsonConfiguration.cs index 8cdf0c2ec27..b22fa8fb5ef 100644 --- a/seed/csharp-sdk/simple-fhir/src/SeedApi/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/simple-fhir/src/SeedApi/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedApi.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0e58215e0fa --- /dev/null +++ b/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedSingleUrlEnvironmentDefault.Core; + +namespace SeedSingleUrlEnvironmentDefault.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/OneOfSerializerTests.cs index 1e8c532a119..a7013fa7ec7 100644 --- a/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedSingleUrlEnvironmentDefault.Core; -namespace SeedSingleUrlEnvironmentDefault.Test.Core; +namespace SeedSingleUrlEnvironmentDefault.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..3ccee2ca93a --- /dev/null +++ b/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedSingleUrlEnvironmentDefault.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/JsonConfiguration.cs b/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/JsonConfiguration.cs index 1531acaa29d..55f8e377705 100644 --- a/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/single-url-environment-default/src/SeedSingleUrlEnvironmentDefault/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedSingleUrlEnvironmentDefault.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..44923808fb9 --- /dev/null +++ b/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedSingleUrlEnvironmentNoDefault.Core; + +namespace SeedSingleUrlEnvironmentNoDefault.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/OneOfSerializerTests.cs index 4e9346ba473..18174ec5053 100644 --- a/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedSingleUrlEnvironmentNoDefault.Core; -namespace SeedSingleUrlEnvironmentNoDefault.Test.Core; +namespace SeedSingleUrlEnvironmentNoDefault.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..f8896696441 --- /dev/null +++ b/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedSingleUrlEnvironmentNoDefault.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/JsonConfiguration.cs b/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/JsonConfiguration.cs index 06ff508acb2..d857b74eb75 100644 --- a/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/single-url-environment-no-default/src/SeedSingleUrlEnvironmentNoDefault/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedSingleUrlEnvironmentNoDefault.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/streaming-parameter/src/SeedStreaming.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/streaming-parameter/src/SeedStreaming.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0c5144133a3 --- /dev/null +++ b/seed/csharp-sdk/streaming-parameter/src/SeedStreaming.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedStreaming.Core; + +namespace SeedStreaming.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/streaming-parameter/src/SeedStreaming.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/streaming-parameter/src/SeedStreaming.Test/Core/Json/OneOfSerializerTests.cs index d291cc246c3..6caae06a78c 100644 --- a/seed/csharp-sdk/streaming-parameter/src/SeedStreaming.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/streaming-parameter/src/SeedStreaming.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedStreaming.Core; -namespace SeedStreaming.Test.Core; +namespace SeedStreaming.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/streaming-parameter/src/SeedStreaming/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/streaming-parameter/src/SeedStreaming/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..8668423896d --- /dev/null +++ b/seed/csharp-sdk/streaming-parameter/src/SeedStreaming/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedStreaming.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/streaming-parameter/src/SeedStreaming/Core/JsonConfiguration.cs b/seed/csharp-sdk/streaming-parameter/src/SeedStreaming/Core/JsonConfiguration.cs index 7f08fa1b680..f7bf5203666 100644 --- a/seed/csharp-sdk/streaming-parameter/src/SeedStreaming/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/streaming-parameter/src/SeedStreaming/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedStreaming.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/streaming/src/SeedStreaming.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/streaming/src/SeedStreaming.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..0c5144133a3 --- /dev/null +++ b/seed/csharp-sdk/streaming/src/SeedStreaming.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedStreaming.Core; + +namespace SeedStreaming.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/streaming/src/SeedStreaming.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/streaming/src/SeedStreaming.Test/Core/Json/OneOfSerializerTests.cs index d291cc246c3..6caae06a78c 100644 --- a/seed/csharp-sdk/streaming/src/SeedStreaming.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/streaming/src/SeedStreaming.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedStreaming.Core; -namespace SeedStreaming.Test.Core; +namespace SeedStreaming.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/streaming/src/SeedStreaming/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/streaming/src/SeedStreaming/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..8668423896d --- /dev/null +++ b/seed/csharp-sdk/streaming/src/SeedStreaming/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedStreaming.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/streaming/src/SeedStreaming/Core/JsonConfiguration.cs b/seed/csharp-sdk/streaming/src/SeedStreaming/Core/JsonConfiguration.cs index 7f08fa1b680..f7bf5203666 100644 --- a/seed/csharp-sdk/streaming/src/SeedStreaming/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/streaming/src/SeedStreaming/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedStreaming.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/trace/src/SeedTrace.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/trace/src/SeedTrace.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..b1bcc482557 --- /dev/null +++ b/seed/csharp-sdk/trace/src/SeedTrace.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedTrace.Core; + +namespace SeedTrace.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/trace/src/SeedTrace.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/trace/src/SeedTrace.Test/Core/Json/OneOfSerializerTests.cs index fa9d06a0015..cdd5a02c1a6 100644 --- a/seed/csharp-sdk/trace/src/SeedTrace.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/trace/src/SeedTrace.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedTrace.Core; -namespace SeedTrace.Test.Core; +namespace SeedTrace.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/trace/src/SeedTrace/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/trace/src/SeedTrace/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..4b09f52cd94 --- /dev/null +++ b/seed/csharp-sdk/trace/src/SeedTrace/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedTrace.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/trace/src/SeedTrace/Core/JsonConfiguration.cs b/seed/csharp-sdk/trace/src/SeedTrace/Core/JsonConfiguration.cs index 7ff8d7b5516..ef78107561d 100644 --- a/seed/csharp-sdk/trace/src/SeedTrace/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/trace/src/SeedTrace/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedTrace.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..23df802f69a --- /dev/null +++ b/seed/csharp-sdk/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedUndiscriminatedUnions.Core; + +namespace SeedUndiscriminatedUnions.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/OneOfSerializerTests.cs index 6a5b4a945d1..6bc62cb067c 100644 --- a/seed/csharp-sdk/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/undiscriminated-unions/src/SeedUndiscriminatedUnions.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedUndiscriminatedUnions.Core; -namespace SeedUndiscriminatedUnions.Test.Core; +namespace SeedUndiscriminatedUnions.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..e8eba4aab06 --- /dev/null +++ b/seed/csharp-sdk/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedUndiscriminatedUnions.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/JsonConfiguration.cs b/seed/csharp-sdk/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/JsonConfiguration.cs index 0956b6f3c6b..22f0bac1190 100644 --- a/seed/csharp-sdk/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/undiscriminated-unions/src/SeedUndiscriminatedUnions/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedUndiscriminatedUnions.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/unions/src/SeedUnions.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/unions/src/SeedUnions.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..34292f4939b --- /dev/null +++ b/seed/csharp-sdk/unions/src/SeedUnions.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedUnions.Core; + +namespace SeedUnions.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/unions/src/SeedUnions.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/unions/src/SeedUnions.Test/Core/Json/OneOfSerializerTests.cs index 23d016bcc28..d694be2c591 100644 --- a/seed/csharp-sdk/unions/src/SeedUnions.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/unions/src/SeedUnions.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedUnions.Core; -namespace SeedUnions.Test.Core; +namespace SeedUnions.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/unions/src/SeedUnions/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/unions/src/SeedUnions/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..49dc47f0adf --- /dev/null +++ b/seed/csharp-sdk/unions/src/SeedUnions/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedUnions.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/unions/src/SeedUnions/Core/JsonConfiguration.cs b/seed/csharp-sdk/unions/src/SeedUnions/Core/JsonConfiguration.cs index 820d6a5382e..ee66618a99d 100644 --- a/seed/csharp-sdk/unions/src/SeedUnions/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/unions/src/SeedUnions/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedUnions.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/unknown/src/SeedUnknownAsAny.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/unknown/src/SeedUnknownAsAny.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..c51898d6811 --- /dev/null +++ b/seed/csharp-sdk/unknown/src/SeedUnknownAsAny.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedUnknownAsAny.Core; + +namespace SeedUnknownAsAny.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/unknown/src/SeedUnknownAsAny.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/unknown/src/SeedUnknownAsAny.Test/Core/Json/OneOfSerializerTests.cs index 30c621c055c..56ffa2bd6f3 100644 --- a/seed/csharp-sdk/unknown/src/SeedUnknownAsAny.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/unknown/src/SeedUnknownAsAny.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedUnknownAsAny.Core; -namespace SeedUnknownAsAny.Test.Core; +namespace SeedUnknownAsAny.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/unknown/src/SeedUnknownAsAny/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/unknown/src/SeedUnknownAsAny/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..1a7b4a7dd39 --- /dev/null +++ b/seed/csharp-sdk/unknown/src/SeedUnknownAsAny/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedUnknownAsAny.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/unknown/src/SeedUnknownAsAny/Core/JsonConfiguration.cs b/seed/csharp-sdk/unknown/src/SeedUnknownAsAny/Core/JsonConfiguration.cs index bbee50f2cff..77450a4a645 100644 --- a/seed/csharp-sdk/unknown/src/SeedUnknownAsAny/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/unknown/src/SeedUnknownAsAny/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedUnknownAsAny.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/validation/src/SeedValidation.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/validation/src/SeedValidation.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..07c154cd6d5 --- /dev/null +++ b/seed/csharp-sdk/validation/src/SeedValidation.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedValidation.Core; + +namespace SeedValidation.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/validation/src/SeedValidation.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/validation/src/SeedValidation.Test/Core/Json/OneOfSerializerTests.cs index 58a68219634..53bf8f7c92f 100644 --- a/seed/csharp-sdk/validation/src/SeedValidation.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/validation/src/SeedValidation.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedValidation.Core; -namespace SeedValidation.Test.Core; +namespace SeedValidation.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/validation/src/SeedValidation/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/validation/src/SeedValidation/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..90141301121 --- /dev/null +++ b/seed/csharp-sdk/validation/src/SeedValidation/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedValidation.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/validation/src/SeedValidation/Core/JsonConfiguration.cs b/seed/csharp-sdk/validation/src/SeedValidation/Core/JsonConfiguration.cs index f59710f89fb..0cfa8d016a8 100644 --- a/seed/csharp-sdk/validation/src/SeedValidation/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/validation/src/SeedValidation/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedValidation.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/variables/src/SeedVariables.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/variables/src/SeedVariables.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..23e809dd72f --- /dev/null +++ b/seed/csharp-sdk/variables/src/SeedVariables.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedVariables.Core; + +namespace SeedVariables.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/variables/src/SeedVariables.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/variables/src/SeedVariables.Test/Core/Json/OneOfSerializerTests.cs index 3d60c1dd0ce..6131eedd928 100644 --- a/seed/csharp-sdk/variables/src/SeedVariables.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/variables/src/SeedVariables.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedVariables.Core; -namespace SeedVariables.Test.Core; +namespace SeedVariables.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/variables/src/SeedVariables/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/variables/src/SeedVariables/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..9cab94ada6f --- /dev/null +++ b/seed/csharp-sdk/variables/src/SeedVariables/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedVariables.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/variables/src/SeedVariables/Core/JsonConfiguration.cs b/seed/csharp-sdk/variables/src/SeedVariables/Core/JsonConfiguration.cs index ddf550d0745..2234e340e4a 100644 --- a/seed/csharp-sdk/variables/src/SeedVariables/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/variables/src/SeedVariables/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedVariables.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/version-no-default/src/SeedVersion.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/version-no-default/src/SeedVersion.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..f636c78d519 --- /dev/null +++ b/seed/csharp-sdk/version-no-default/src/SeedVersion.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedVersion.Core; + +namespace SeedVersion.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/version-no-default/src/SeedVersion.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/version-no-default/src/SeedVersion.Test/Core/Json/OneOfSerializerTests.cs index 8e2b45b2e08..8a90d4e45cf 100644 --- a/seed/csharp-sdk/version-no-default/src/SeedVersion.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/version-no-default/src/SeedVersion.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedVersion.Core; -namespace SeedVersion.Test.Core; +namespace SeedVersion.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/version-no-default/src/SeedVersion/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/version-no-default/src/SeedVersion/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..b84bce6b4ea --- /dev/null +++ b/seed/csharp-sdk/version-no-default/src/SeedVersion/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedVersion.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/version-no-default/src/SeedVersion/Core/JsonConfiguration.cs b/seed/csharp-sdk/version-no-default/src/SeedVersion/Core/JsonConfiguration.cs index 1ea339729d3..9505fbfe7b1 100644 --- a/seed/csharp-sdk/version-no-default/src/SeedVersion/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/version-no-default/src/SeedVersion/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedVersion.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/version/src/SeedVersion.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/version/src/SeedVersion.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..f636c78d519 --- /dev/null +++ b/seed/csharp-sdk/version/src/SeedVersion.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedVersion.Core; + +namespace SeedVersion.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/version/src/SeedVersion.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/version/src/SeedVersion.Test/Core/Json/OneOfSerializerTests.cs index 8e2b45b2e08..8a90d4e45cf 100644 --- a/seed/csharp-sdk/version/src/SeedVersion.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/version/src/SeedVersion.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedVersion.Core; -namespace SeedVersion.Test.Core; +namespace SeedVersion.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/version/src/SeedVersion/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/version/src/SeedVersion/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..b84bce6b4ea --- /dev/null +++ b/seed/csharp-sdk/version/src/SeedVersion/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedVersion.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/version/src/SeedVersion/Core/JsonConfiguration.cs b/seed/csharp-sdk/version/src/SeedVersion/Core/JsonConfiguration.cs index 1ea339729d3..9505fbfe7b1 100644 --- a/seed/csharp-sdk/version/src/SeedVersion/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/version/src/SeedVersion/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedVersion.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options; diff --git a/seed/csharp-sdk/websocket/src/SeedWebsocket.Test/Core/Json/JsonAccessAttributeTests.cs b/seed/csharp-sdk/websocket/src/SeedWebsocket.Test/Core/Json/JsonAccessAttributeTests.cs new file mode 100644 index 00000000000..a33d485c6e4 --- /dev/null +++ b/seed/csharp-sdk/websocket/src/SeedWebsocket.Test/Core/Json/JsonAccessAttributeTests.cs @@ -0,0 +1,46 @@ +using global::System.Text.Json.Serialization; +using NUnit.Framework; +using SeedWebsocket.Core; + +namespace SeedWebsocket.Test.Core.Json; + +[TestFixture] +public class JsonAccessAttributeTests +{ + private class MyClass + { + [JsonPropertyName("read_only_prop")] + [JsonAccess(JsonAccessType.ReadOnly)] + public string? ReadOnlyProp { get; set; } + + [JsonPropertyName("write_only_prop")] + [JsonAccess(JsonAccessType.WriteOnly)] + public string? WriteOnlyProp { get; set; } + + [JsonPropertyName("normal_prop")] + public string? NormalProp { get; set; } + } + + [Test] + public void JsonAccessAttribute_ShouldWorkAsExpected() + { + const string json = + """ { "read_only_prop": "read", "write_only_prop": "write", "normal_prop": "normal_prop" } """; + var obj = JsonUtils.Deserialize(json); + + Assert.Multiple(() => + { + Assert.That(obj.ReadOnlyProp, Is.EqualTo("read")); + Assert.That(obj.WriteOnlyProp, Is.Null); + Assert.That(obj.NormalProp, Is.EqualTo("normal_prop")); + }); + + obj.WriteOnlyProp = "write"; + obj.NormalProp = "new_value"; + + var serializedJson = JsonUtils.Serialize(obj); + const string expectedJson = + "{\n \"write_only_prop\": \"write\",\n \"normal_prop\": \"new_value\"\n}"; + Assert.That(serializedJson, Is.EqualTo(expectedJson)); + } +} diff --git a/seed/csharp-sdk/websocket/src/SeedWebsocket.Test/Core/Json/OneOfSerializerTests.cs b/seed/csharp-sdk/websocket/src/SeedWebsocket.Test/Core/Json/OneOfSerializerTests.cs index 2fa7712800e..7493f21beb1 100644 --- a/seed/csharp-sdk/websocket/src/SeedWebsocket.Test/Core/Json/OneOfSerializerTests.cs +++ b/seed/csharp-sdk/websocket/src/SeedWebsocket.Test/Core/Json/OneOfSerializerTests.cs @@ -4,7 +4,7 @@ using OneOf; using SeedWebsocket.Core; -namespace SeedWebsocket.Test.Core; +namespace SeedWebsocket.Test.Core.Json; [TestFixture] [Parallelizable(ParallelScope.All)] diff --git a/seed/csharp-sdk/websocket/src/SeedWebsocket/Core/JsonAccessAttribute.cs b/seed/csharp-sdk/websocket/src/SeedWebsocket/Core/JsonAccessAttribute.cs new file mode 100644 index 00000000000..22eb1e6e24e --- /dev/null +++ b/seed/csharp-sdk/websocket/src/SeedWebsocket/Core/JsonAccessAttribute.cs @@ -0,0 +1,13 @@ +namespace SeedWebsocket.Core; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] +internal class JsonAccessAttribute(JsonAccessType accessType) : Attribute +{ + internal JsonAccessType AccessType { get; init; } = accessType; +} + +internal enum JsonAccessType +{ + ReadOnly, + WriteOnly, +} diff --git a/seed/csharp-sdk/websocket/src/SeedWebsocket/Core/JsonConfiguration.cs b/seed/csharp-sdk/websocket/src/SeedWebsocket/Core/JsonConfiguration.cs index 6a1965270be..0c400e2f633 100644 --- a/seed/csharp-sdk/websocket/src/SeedWebsocket/Core/JsonConfiguration.cs +++ b/seed/csharp-sdk/websocket/src/SeedWebsocket/Core/JsonConfiguration.cs @@ -1,5 +1,6 @@ -using System.Text.Json; -using System.Text.Json.Serialization; +using global::System.Text.Json; +using global::System.Text.Json.Serialization; +using global::System.Text.Json.Serialization.Metadata; namespace SeedWebsocket.Core; @@ -11,16 +12,54 @@ static JsonOptions() { var options = new JsonSerializerOptions { - Converters = - { - new DateTimeSerializer(), + Converters = { new DateTimeSerializer(), #if USE_PORTABLE_DATE_ONLY new DateOnlyConverter(), #endif - new OneOfSerializer(), - }, + new OneOfSerializer() }, +#if DEBUG WriteIndented = true, +#endif DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + TypeInfoResolver = new DefaultJsonTypeInfoResolver + { + Modifiers = + { + static typeInfo => + { + if (typeInfo.Kind != JsonTypeInfoKind.Object) + return; + + foreach (var propertyInfo in typeInfo.Properties) + { + var jsonAccessAttribute = propertyInfo + .AttributeProvider?.GetCustomAttributes( + typeof(JsonAccessAttribute), + true + ) + .OfType() + .FirstOrDefault(); + + if (jsonAccessAttribute != null) + { + propertyInfo.IsRequired = false; + switch (jsonAccessAttribute.AccessType) + { + case JsonAccessType.ReadOnly: + propertyInfo.Get = null; + propertyInfo.ShouldSerialize = (_, _) => false; + break; + case JsonAccessType.WriteOnly: + propertyInfo.Set = null; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + }, + }, + }, }; ConfigureJsonSerializerOptions(options); JsonSerializerOptions = options;