From 41fe77291684d9e393f2899bca0e8da0e10ca4ef Mon Sep 17 00:00:00 2001 From: Jasmin Oster Date: Fri, 19 Jan 2024 10:32:48 +0100 Subject: [PATCH] SIANXSVC-1193: dotnet-e5e does not handle binary requests as expected Closes SIANXSVC-1193 --- src/Anexia.E5E.Tests/Anexia.E5E.Tests.csproj | 10 ++ .../BinaryRequestIntegrationTests.cs | 117 ++++++++++++++++++ .../Serialization/SerializationTests.cs | 16 ++- ...estName=simple binary request.verified.txt | 2 +- ...stName=simple binary response.verified.txt | 2 +- src/Anexia.E5E.Tests/TestData/TestData.cs | 17 +++ .../binary_request_unknown_content_type.json | 18 +++ .../binary_request_with_multiple_files.json | 28 +++++ .../TestHelpers/TestRequestBuilder.cs | 5 +- .../E5EMultipleFilesInFormDataException.cs | 21 ++++ .../E5EObsoleteCodeInUseException.cs | 18 +++ src/Anexia.E5E/Functions/E5EEvent.cs | 41 +++++- src/Anexia.E5E/Functions/E5EFileData.cs | 57 +++++++++ src/Anexia.E5E/Functions/E5EResponse.cs | 58 ++++++++- .../Serialization/E5ESerializationContext.cs | 2 + 15 files changed, 399 insertions(+), 13 deletions(-) create mode 100644 src/Anexia.E5E.Tests/Integration/BinaryRequestIntegrationTests.cs create mode 100644 src/Anexia.E5E.Tests/TestData/TestData.cs create mode 100644 src/Anexia.E5E.Tests/TestData/binary_request_unknown_content_type.json create mode 100644 src/Anexia.E5E.Tests/TestData/binary_request_with_multiple_files.json create mode 100644 src/Anexia.E5E/Exceptions/E5EMultipleFilesInFormDataException.cs create mode 100644 src/Anexia.E5E/Exceptions/E5EObsoleteCodeInUseException.cs create mode 100644 src/Anexia.E5E/Functions/E5EFileData.cs diff --git a/src/Anexia.E5E.Tests/Anexia.E5E.Tests.csproj b/src/Anexia.E5E.Tests/Anexia.E5E.Tests.csproj index da22ec7..7f928bc 100644 --- a/src/Anexia.E5E.Tests/Anexia.E5E.Tests.csproj +++ b/src/Anexia.E5E.Tests/Anexia.E5E.Tests.csproj @@ -4,6 +4,7 @@ net6.0;net8.0 enable false + 12 @@ -27,4 +28,13 @@ + + + PreserveNewest + + + PreserveNewest + + + diff --git a/src/Anexia.E5E.Tests/Integration/BinaryRequestIntegrationTests.cs b/src/Anexia.E5E.Tests/Integration/BinaryRequestIntegrationTests.cs new file mode 100644 index 0000000..5c512c4 --- /dev/null +++ b/src/Anexia.E5E.Tests/Integration/BinaryRequestIntegrationTests.cs @@ -0,0 +1,117 @@ +// -------------------------------------------------------------------------------------------- +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// -------------------------------------------------------------------------------------------- + +using System.Threading.Tasks; + +using Anexia.E5E.Exceptions; +using Anexia.E5E.Functions; +using Anexia.E5E.Tests.TestHelpers; + +using static Anexia.E5E.Tests.TestData.TestData; + +using Xunit; +using Xunit.Abstractions; + +namespace Anexia.E5E.Tests.Integration; + +public sealed class BinaryRequestIntegrationTests(ITestOutputHelper outputHelper) : IntegrationTestBase(outputHelper) +{ + [Fact] + public async Task DecodeToBytesThrowsForMultipleFiles() + { + await Host.StartWithTestEntrypointAsync(request => + { + Assert.Throws(() => request.Event.AsBytes()); + return null!; + }); + await Host.WriteOnceAsync(BinaryRequestWithMultipleFiles); + } + + [Fact] + public async Task MultipleFilesAreProperlyDecoded() + { + await Host.StartWithTestEntrypointAsync(request => + { + var files = request.Event.AsFiles(); + Assert.Collection(files, first => + { + Assert.NotNull(first); + Assert.Equal(first, new E5EFileData + { + Base64Encoded = "SGVsbG8gd29ybGQh", + FileSizeInBytes = 12, + Filename = "my-file-1.name", + ContentType = "application/my-content-type-1", + Charset = "utf-8", + }); + }, second => + { + Assert.NotNull(second); + Assert.Equal(second, new E5EFileData + { + Base64Encoded = "SGVsbG8gd29ybGQh", + FileSizeInBytes = 12, + Filename = "my-file-2.name", + ContentType = "application/my-content-type-2", + Charset = "utf-8", + }); + }); + return null!; + }); + await Host.WriteOnceAsync(BinaryRequestWithMultipleFiles); + } + + [Fact] + public async Task UnknownContentType() + { + await Host.StartWithTestEntrypointAsync(request => + { + Assert.Equal("SGVsbG8gd29ybGQh"u8.ToArray(), request.Event.AsBytes()); + return null!; + }); + await Host.WriteOnceAsync(BinaryRequestWithUnknownContentType); + } + + [Fact] + public async Task FallbackForByteArrayReturnsValidResponse() + { + // act +#pragma warning disable CS0618 // Type or member is obsolete + await Host.StartWithTestEntrypointAsync(_ => E5EResponse.From("SGVsbG8gd29ybGQh"u8.ToArray())); +#pragma warning restore CS0618 // Type or member is obsolete + var response = await Host.WriteOnceAsync(x => x.WithData("test")); + + // assert + const string expected = + """ + {"data":{"binary":"SGVsbG8gd29ybGQh","type":"binary","size":0,"name":"dotnet-e5e-binary-response.blob","content_type":"application/octet-stream","charset":"utf-8"},"type":"binary"} + """; + Assert.Contains(expected, response.Stdout); + } + + [Fact] + public async Task FileDataReturnsValidResponse() + { + // act + await Host.StartWithTestEntrypointAsync(_ => E5EResponse.From(new E5EFileData + { + Base64Encoded = "SGVsbG8gd29ybGQh", + Type = "binary", + FileSizeInBytes = 16, + Filename = "hello-world.txt", + ContentType = "text/plain", + Charset = "utf-8", + })); + var response = await Host.WriteOnceAsync(x => x.WithData("test")); + + // assert + const string expected = + """ + {"data":{"binary":"SGVsbG8gd29ybGQh","type":"binary","size":16,"name":"hello-world.txt","content_type":"text/plain","charset":"utf-8"},"type":"binary"} + """; + Assert.Contains(expected, response.Stdout); + } +} diff --git a/src/Anexia.E5E.Tests/Serialization/SerializationTests.cs b/src/Anexia.E5E.Tests/Serialization/SerializationTests.cs index 92ef913..5056b31 100644 --- a/src/Anexia.E5E.Tests/Serialization/SerializationTests.cs +++ b/src/Anexia.E5E.Tests/Serialization/SerializationTests.cs @@ -99,8 +99,11 @@ public void ResponseSerializationWorksBidirectional(string _, E5EResponse input) public void ResponseSerializationRecognisesCorrectType() { Assert.Equal(E5EResponseType.Text, E5EResponse.From("test").Type); - Assert.Equal(E5EResponseType.Binary, E5EResponse.From(Encoding.UTF8.GetBytes("test")).Type); - Assert.Equal(E5EResponseType.Binary, E5EResponse.From(Encoding.UTF8.GetBytes("test").AsEnumerable()).Type); +#pragma warning disable CS0618 // Type or member is obsolete + Assert.Equal(E5EResponseType.Binary, E5EResponse.From("test"u8.ToArray()).Type); + Assert.Equal(E5EResponseType.Binary, E5EResponse.From("test"u8.ToArray().AsEnumerable()).Type); +#pragma warning restore CS0618 // Type or member is obsolete + Assert.Equal(E5EResponseType.Binary, E5EResponse.From(new E5EFileData()).Type); Assert.Equal(E5EResponseType.StructuredObject, E5EResponse.From(new E5ERuntimeMetadata()).Type); } @@ -180,7 +183,10 @@ private class RequestSerializationTestsData : IEnumerable private readonly Dictionary _tests = new() { { "simple text request", new TestRequestBuilder().WithData("test").BuildEvent() }, - { "simple binary request", new TestRequestBuilder().WithData(Encoding.UTF8.GetBytes("test")).BuildEvent() }, + { + "simple binary request", + new TestRequestBuilder().WithData(new E5EFileData { Base64Encoded = "aGVsbG8=" }).BuildEvent() + }, { "simple object request", new TestRequestBuilder().WithData(new Dictionary { { "test", "value" } }).BuildEvent() @@ -211,7 +217,9 @@ private class ResponseSerializationTestsData : IEnumerable private readonly Dictionary _tests = new() { { "simple text response", E5EResponse.From("test") }, - { "simple binary response", E5EResponse.From(Encoding.UTF8.GetBytes("test")) }, +#pragma warning disable CS0618 // Type or member is obsolete + { "simple binary response", E5EResponse.From("aGVsbG8="u8.ToArray()) }, +#pragma warning restore CS0618 // Type or member is obsolete { "simple object response", E5EResponse.From(new Dictionary { { "a", 1 }, { "b", 2 } }) }, { "text response with headers and status code", E5EResponse.From("test", HttpStatusCode.Moved, diff --git a/src/Anexia.E5E.Tests/Serialization/snapshots/SerializationTests.RequestSerializationMatchesSnapshot_testName=simple binary request.verified.txt b/src/Anexia.E5E.Tests/Serialization/snapshots/SerializationTests.RequestSerializationMatchesSnapshot_testName=simple binary request.verified.txt index 9401660..7dcb3b0 100644 --- a/src/Anexia.E5E.Tests/Serialization/snapshots/SerializationTests.RequestSerializationMatchesSnapshot_testName=simple binary request.verified.txt +++ b/src/Anexia.E5E.Tests/Serialization/snapshots/SerializationTests.RequestSerializationMatchesSnapshot_testName=simple binary request.verified.txt @@ -1 +1 @@ -{"type":"binary","data":"dGVzdA=="} +{"type":"binary","data":{"binary":"aGVsbG8=","type":"binary","size":0,"name":null,"content_type":null,"charset":null}} \ No newline at end of file diff --git a/src/Anexia.E5E.Tests/Serialization/snapshots/SerializationTests.ResponseSerializationMatchesSnapshot_testName=simple binary response.verified.txt b/src/Anexia.E5E.Tests/Serialization/snapshots/SerializationTests.ResponseSerializationMatchesSnapshot_testName=simple binary response.verified.txt index 9027409..0d09e27 100644 --- a/src/Anexia.E5E.Tests/Serialization/snapshots/SerializationTests.ResponseSerializationMatchesSnapshot_testName=simple binary response.verified.txt +++ b/src/Anexia.E5E.Tests/Serialization/snapshots/SerializationTests.ResponseSerializationMatchesSnapshot_testName=simple binary response.verified.txt @@ -1 +1 @@ -{"data":"dGVzdA==","type":"binary"} +{"data":{"binary":"aGVsbG8=","type":"binary","size":0,"name":"dotnet-e5e-binary-response.blob","content_type":"application/octet-stream","charset":"utf-8"},"type":"binary"} \ No newline at end of file diff --git a/src/Anexia.E5E.Tests/TestData/TestData.cs b/src/Anexia.E5E.Tests/TestData/TestData.cs new file mode 100644 index 0000000..cbffed5 --- /dev/null +++ b/src/Anexia.E5E.Tests/TestData/TestData.cs @@ -0,0 +1,17 @@ +// -------------------------------------------------------------------------------------------- +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// -------------------------------------------------------------------------------------------- + +using System.IO; + +namespace Anexia.E5E.Tests.TestData; + +internal static class TestData +{ + private static string ReadTestDataFile(string path) => File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "TestData", path)); + + internal static string BinaryRequestWithMultipleFiles => ReadTestDataFile("binary_request_with_multiple_files.json"); + internal static string BinaryRequestWithUnknownContentType => ReadTestDataFile("binary_request_unknown_content_type.json"); +} diff --git a/src/Anexia.E5E.Tests/TestData/binary_request_unknown_content_type.json b/src/Anexia.E5E.Tests/TestData/binary_request_unknown_content_type.json new file mode 100644 index 0000000..ac5d6c6 --- /dev/null +++ b/src/Anexia.E5E.Tests/TestData/binary_request_unknown_content_type.json @@ -0,0 +1,18 @@ +{ + "context": { + "type": "integration-test", + "async": true, + "date": "2024-01-01T00:00:00Z" + }, + "event": { + "type": "binary", + "data": { + "binary": "SGVsbG8gd29ybGQh", + "type": "binary", + "name": "my-file-1.name", + "size": 12, + "content_type": "application/my-content-type-1", + "charset": "utf-8" + } + } +} diff --git a/src/Anexia.E5E.Tests/TestData/binary_request_with_multiple_files.json b/src/Anexia.E5E.Tests/TestData/binary_request_with_multiple_files.json new file mode 100644 index 0000000..e24bb6d --- /dev/null +++ b/src/Anexia.E5E.Tests/TestData/binary_request_with_multiple_files.json @@ -0,0 +1,28 @@ +{ + "context": { + "type": "integration-test", + "async": true, + "date": "2024-01-01T00:00:00Z" + }, + "event": { + "type": "binary", + "data": [ + { + "binary": "SGVsbG8gd29ybGQh", + "type": "binary", + "name": "my-file-1.name", + "size": 12, + "content_type": "application/my-content-type-1", + "charset": "utf-8" + }, + { + "binary": "SGVsbG8gd29ybGQh", + "type": "binary", + "name": "my-file-2.name", + "size": 12, + "content_type": "application/my-content-type-2", + "charset": "utf-8" + } + ] + } +} diff --git a/src/Anexia.E5E.Tests/TestHelpers/TestRequestBuilder.cs b/src/Anexia.E5E.Tests/TestHelpers/TestRequestBuilder.cs index e90d144..a40d3b8 100644 --- a/src/Anexia.E5E.Tests/TestHelpers/TestRequestBuilder.cs +++ b/src/Anexia.E5E.Tests/TestHelpers/TestRequestBuilder.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Text.Json; @@ -17,7 +18,9 @@ public TestRequestBuilder WithData(T data) _requestType = data switch { string => E5ERequestDataType.Text, - IEnumerable => E5ERequestDataType.Binary, + IEnumerable => throw new InvalidOperationException( + $"E5E does not compose binary requests just from the bytes. Please convert this call to use {nameof(E5EFileData)} instead."), + E5EFileData => E5ERequestDataType.Binary, _ => E5ERequestDataType.StructuredObject, }; _data = JsonSerializer.SerializeToElement(data); diff --git a/src/Anexia.E5E/Exceptions/E5EMultipleFilesInFormDataException.cs b/src/Anexia.E5E/Exceptions/E5EMultipleFilesInFormDataException.cs new file mode 100644 index 0000000..20d6436 --- /dev/null +++ b/src/Anexia.E5E/Exceptions/E5EMultipleFilesInFormDataException.cs @@ -0,0 +1,21 @@ +// -------------------------------------------------------------------------------------------- +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// -------------------------------------------------------------------------------------------- + +using Anexia.E5E.Functions; + +namespace Anexia.E5E.Exceptions; + +/// +/// Exception that is thrown if the method is called, but there is more than one +/// file attached to the request. +/// +public sealed class E5EMultipleFilesInFormDataException : E5EException +{ + internal E5EMultipleFilesInFormDataException() : base( + $"There were multiple files attached to this request, so there's no unique binary data. Please use the {nameof(E5EEvent)}.{nameof(E5EEvent.AsFiles)} method instead.") + { + } +} diff --git a/src/Anexia.E5E/Exceptions/E5EObsoleteCodeInUseException.cs b/src/Anexia.E5E/Exceptions/E5EObsoleteCodeInUseException.cs new file mode 100644 index 0000000..315db25 --- /dev/null +++ b/src/Anexia.E5E/Exceptions/E5EObsoleteCodeInUseException.cs @@ -0,0 +1,18 @@ +// -------------------------------------------------------------------------------------------- +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// -------------------------------------------------------------------------------------------- + +namespace Anexia.E5E.Exceptions; + +/// +/// Exception that is thrown if obsolete code is in usage. +/// Usually this shouldn't not happen, as the APIs are backwards compatible. If it gets thrown nonetheless, this indicates a bug in the library. +/// +public sealed class E5EObsoleteCodeInUseException : E5EException +{ + internal E5EObsoleteCodeInUseException(string message) : base(message) + { + } +} diff --git a/src/Anexia.E5E/Functions/E5EEvent.cs b/src/Anexia.E5E/Functions/E5EEvent.cs index bc85a07..2a5ad9c 100644 --- a/src/Anexia.E5E/Functions/E5EEvent.cs +++ b/src/Anexia.E5E/Functions/E5EEvent.cs @@ -14,7 +14,8 @@ namespace Anexia.E5E.Functions; /// The data, not processed. /// The request headers, if any. /// The request parameters, if any. -public record E5EEvent(E5ERequestDataType Type, +public record E5EEvent( + E5ERequestDataType Type, JsonElement? Data = null, E5EHttpHeaders? RequestHeaders = null, E5ERequestParameters? Params = null) @@ -63,15 +64,45 @@ public record E5EEvent(E5ERequestDataType Type, } /// - /// Returns the value as byte enumerable. + /// Returns the bytes of the attached file. /// /// - /// Thrown if is not - /// . + /// Thrown if is not . /// + /// Thrown if there are multiple files attached to this request. public byte[]? AsBytes() { E5EInvalidConversionException.ThrowIfNotMatch(E5ERequestDataType.Binary, Type); - return As(E5ESerializationContext.Default.ByteArray); + if (Data.GetValueOrDefault().ValueKind == JsonValueKind.Array) + throw new E5EMultipleFilesInFormDataException(); + +#if NET8_0_OR_GREATER + var fileData = As(E5ESerializationContext.Default.E5EFileData); +#else + var fileData = As(); +#endif + + return fileData?.GetBytes().ToArray(); + } + + /// + /// If this request is a multipart/form-data request, all files attached to this request are deserialized. + /// + /// + /// + public IEnumerable AsFiles() + { + E5EInvalidConversionException.ThrowIfNotMatch(E5ERequestDataType.Binary, Type); + return Data.GetValueOrDefault().ValueKind switch + { +#if NET8_0_OR_GREATER + JsonValueKind.Object => new[] { As(E5ESerializationContext.Default.E5EFileData)! }, + JsonValueKind.Array => As(E5ESerializationContext.Default.IEnumerableE5EFileData), +#else + JsonValueKind.Object => new[] { As()! }, + JsonValueKind.Array => As>(), +#endif + _ => null, + } ?? Enumerable.Empty(); } } diff --git a/src/Anexia.E5E/Functions/E5EFileData.cs b/src/Anexia.E5E/Functions/E5EFileData.cs new file mode 100644 index 0000000..8d4188d --- /dev/null +++ b/src/Anexia.E5E/Functions/E5EFileData.cs @@ -0,0 +1,57 @@ +// -------------------------------------------------------------------------------------------- +// +// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved. +// +// -------------------------------------------------------------------------------------------- + +using System.Text.Json.Serialization; + +namespace Anexia.E5E.Functions; + +/// +/// Contains information about the files that were attached to this request. +/// +/// For requests, it requires that it has the type of or . +public record E5EFileData +{ + /// + /// The raw base64-encoded data. + /// + [JsonPropertyName("binary")] + public string Base64Encoded { get; init; } = ""; + + /// + /// The type of this binary, usually just binary. + /// + [JsonPropertyName("type")] + public string Type { get; init; } = "binary"; + + /// + /// The size of the file in bytes. Can be zero if it cannot be determined reliably. + /// + [JsonPropertyName("size")] + public long FileSizeInBytes { get; init; } + + /// + /// The optional filename of the file. + /// + [JsonPropertyName("name")] + public string? Filename { get; init; } + + /// + /// The content type of the file. + /// + /// + /// For responses, the Content-Type header is set automatically by the E5E engine to this value. + /// + [JsonPropertyName("content_type")] + public string? ContentType { get; init; } + + /// + /// The charset of the file. Recommended value is utf-8. + /// + [JsonPropertyName("charset")] + public string? Charset { get; init; } + + internal IEnumerable GetBytes() => System.Text.Encoding.UTF8.GetBytes(Base64Encoded); +} diff --git a/src/Anexia.E5E/Functions/E5EResponse.cs b/src/Anexia.E5E/Functions/E5EResponse.cs index 7b33681..ef79478 100644 --- a/src/Anexia.E5E/Functions/E5EResponse.cs +++ b/src/Anexia.E5E/Functions/E5EResponse.cs @@ -2,6 +2,8 @@ using System.Net; using System.Text.Json; +using Anexia.E5E.Exceptions; + namespace Anexia.E5E.Functions; /// @@ -50,7 +52,9 @@ public static E5EResponse From(T data, HttpStatusCode? status = null, E5EHttp Type = data switch { IEnumerable => E5EResponseType.Text, - IEnumerable => E5EResponseType.Binary, + E5EFileData => E5EResponseType.Binary, + IEnumerable => throw new E5EObsoleteCodeInUseException( + $"Creating responses from byte enumerations is unsupported, please use the {nameof(E5EFileData)} for this."), _ => E5EResponseType.StructuredObject, }, Data = JsonSerializer.SerializeToElement(data), @@ -58,4 +62,56 @@ public static E5EResponse From(T data, HttpStatusCode? status = null, E5EHttp ResponseHeaders = responseHeaders, }; } + + /// + /// Creates a new from the given object with the type + /// . + /// + /// The data object. + /// An optional HTTP status code. + /// An optional list of HTTP headers. + /// A valid with the given data. + [RequiresUnreferencedCode( + "This helper relies on runtime reflection for the JSON serialization. Initialize the E5EResponse by yourself for AOT.")] + [Obsolete( + $"Creating responses from byte enumerations is unsupported, please convert the calls to use {nameof(E5EFileData)} instead.")] +#if NET8_0_OR_GREATER + [RequiresDynamicCode( + "This helper relies on runtime reflection for the JSON serialization. Initialize the E5EResponse by yourself for AOT.")] +#endif + public static E5EResponse From(IEnumerable data, HttpStatusCode? status = null, + E5EHttpHeaders? responseHeaders = null) + { + return From(data.ToArray()); + } + + /// + /// Creates a new from the given object with the type + /// . + /// + /// The data object. + /// An optional HTTP status code. + /// An optional list of HTTP headers. + /// A valid with the given data. + [RequiresUnreferencedCode( + "This helper relies on runtime reflection for the JSON serialization. Initialize the E5EResponse by yourself for AOT.")] + [Obsolete( + $"Creating responses from byte enumerations is unsupported, please convert the calls to use {nameof(E5EFileData)} instead.")] +#if NET8_0_OR_GREATER + [RequiresDynamicCode( + "This helper relies on runtime reflection for the JSON serialization. Initialize the E5EResponse by yourself for AOT.")] +#endif + public static E5EResponse From(byte[] data, HttpStatusCode? status = null, + E5EHttpHeaders? responseHeaders = null) + { + var decoded = System.Text.Encoding.UTF8.GetString(data); + + return From(new E5EFileData + { + Base64Encoded = decoded, + Charset = "utf-8", + ContentType = "application/octet-stream", + Filename = "dotnet-e5e-binary-response.blob", + }, status, responseHeaders); + } } diff --git a/src/Anexia.E5E/Serialization/E5ESerializationContext.cs b/src/Anexia.E5E/Serialization/E5ESerializationContext.cs index cc28ed0..8909b09 100644 --- a/src/Anexia.E5E/Serialization/E5ESerializationContext.cs +++ b/src/Anexia.E5E/Serialization/E5ESerializationContext.cs @@ -15,6 +15,8 @@ namespace Anexia.E5E.Serialization; [JsonSerializable(typeof(E5ERequest))] [JsonSerializable(typeof(E5EResponse))] [JsonSerializable(typeof(E5ERuntimeMetadata))] +[JsonSerializable(typeof(E5EFileData))] +[JsonSerializable(typeof(IEnumerable))] [JsonSourceGenerationOptions( JsonSerializerDefaults.General, IgnoreReadOnlyProperties = false,