diff --git a/docs/usage/openapi-client.md b/docs/usage/openapi-client.md index f8a4dd4cec..14fbe8e822 100644 --- a/docs/usage/openapi-client.md +++ b/docs/usage/openapi-client.md @@ -7,7 +7,7 @@ After [enabling OpenAPI](~/usage/openapi.md), you can generate a typed JSON:API > [client libraries](https://jsonapi.org/implementations/#client-libraries). The following code generators are supported, though you may try others as well: -- [NSwag](https://github.com/RicoSuter/NSwag) (v14.1 or higher): Produces clients for C# and TypeScript +- [NSwag](https://github.com/RicoSuter/NSwag) (v14.1 or higher): Produces clients for C# (requires `Newtonsoft.Json`) and TypeScript - [Kiota](https://learn.microsoft.com/en-us/openapi/kiota/overview): Produces clients for C#, Go, Java, PHP, Python, Ruby, Swift and TypeScript # [NSwag](#tab/nswag) @@ -21,7 +21,7 @@ dotnet add package JsonApiDotNetCore.OpenApi.Client.NSwag # [Kiota](#tab/kiota) -For C# clients, we provide an additional package that provides workarounds for bugs in Kiota. +For C# clients, we provide an additional package that provides workarounds for bugs in Kiota, as well as MSBuild integration. To add it to your project, run the following command: ``` @@ -60,27 +60,6 @@ The following steps describe how to generate and use a JSON:API client in C#, co dotnet add package JsonApiDotNetCore.OpenApi.Client.NSwag ``` -1. Add the following glue code to connect our package with your generated code. - - > [!NOTE] - > The class name must be the same as specified in step 2. - > If you also specified a namespace, put this class in the same namespace. - > For example, add `namespace GeneratedCode;` below the `using` lines. - - ```c# - using JsonApiDotNetCore.OpenApi.Client.NSwag; - using Newtonsoft.Json; - - partial class ExampleApiClient : JsonApiClient - { - partial void Initialize() - { - _instanceSettings = new JsonSerializerSettings(_settings.Value); - SetSerializerSettingsForJsonApi(_instanceSettings); - } - } - ``` - 1. Add code that calls one of your JSON:API endpoints. ```c# @@ -108,33 +87,32 @@ The following steps describe how to generate and use a JSON:API client in C#, co Data = new DataInUpdatePersonRequest { Id = "1", - Attributes = new AttributesInUpdatePersonRequest + // Using TrackChangesFor to send "firstName: null" instead of omitting it. + Attributes = new TrackChangesFor(_apiClient) { - LastName = "Doe" - } + Initializer = + { + FirstName = null, + LastName = "Doe" + } + }.Initializer } }; - // This line results in sending "firstName: null" instead of omitting it. - using (apiClient.WithPartialAttributeSerialization( - updatePersonRequest, person => person.FirstName)) - { - // Workaround for https://github.com/RicoSuter/NSwag/issues/2499. - await ApiResponse.TranslateAsync(() => - apiClient.PatchPersonAsync(updatePersonRequest.Data.Id, updatePersonRequest)); - - // The sent request looks like this: - // { - // "data": { - // "type": "people", - // "id": "1", - // "attributes": { - // "firstName": null, - // "lastName": "Doe" - // } - // } - // } - } + await ApiResponse.TranslateAsync(async () => + await _apiClient.PatchPersonAsync(updatePersonRequest.Data.Id, updatePersonRequest)); + + // The sent request looks like this: + // { + // "data": { + // "type": "people", + // "id": "1", + // "attributes": { + // "firstName": null, + // "lastName": "Doe" + // } + // } + // } ``` > [!TIP] @@ -146,9 +124,7 @@ The following steps describe how to generate and use a JSON:API client in C#, co ### Other IDEs -When using the command line, you can try the [Microsoft.dotnet-openapi Global Tool](https://docs.microsoft.com/en-us/aspnet/core/web-api/microsoft.dotnet-openapi?view=aspnetcore-5.0). - -Alternatively, the following section shows what to add to your client project file directly: +The following section shows what to add to your client project file directly: ```xml @@ -160,9 +136,8 @@ Alternatively, the following section shows what to add to your client project fi http://localhost:14140/swagger/v1/swagger.json - NSwagCSharp ExampleApiClient - ExampleApiClient.cs + %(ClassName).cs ``` @@ -193,20 +168,20 @@ Various switches enable you to tweak the client generation to your needs. See th # [NSwag](#tab/nswag) -The `OpenApiReference` can be customized using various [NSwag-specific MSBuild properties](https://github.com/RicoSuter/NSwag/blob/7d6df3af95081f3f0ed6dee04be8d27faa86f91a/src/NSwag.ApiDescription.Client/NSwag.ApiDescription.Client.props). +The `OpenApiReference` element can be customized using various [NSwag-specific MSBuild properties](https://github.com/RicoSuter/NSwag/blob/7d6df3af95081f3f0ed6dee04be8d27faa86f91a/src/NSwag.ApiDescription.Client/NSwag.ApiDescription.Client.props). See [the source code](https://github.com/RicoSuter/NSwag/blob/master/src/NSwag.Commands/Commands/CodeGeneration/OpenApiToCSharpClientCommand.cs) for their meaning. +The `JsonApiDotNetCore.OpenApi.Client.NSwag` package sets various of these for optimal JSON:API support. > [!NOTE] > Earlier versions of NSwag required the use of `` to specify command-line switches directly. > This is no longer recommended and may conflict with the new MSBuild properties. -For example, the following section puts the generated code in a namespace and generates an interface (handy when writing tests): +For example, the following section puts the generated code in a namespace, makes the client class internal and generates an interface (handy when writing tests): ```xml ExampleProject.GeneratedCode - SalesApiClient - NSwagCSharp + internal true ``` @@ -306,6 +281,7 @@ The [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/ demonstrates how to use them. It uses local IDs to: - Create a new tag - Create a new person +- Update the person to clear an attribute (using `TrackChangesFor`) - Create a new todo-item, tagged with the new tag, and owned by the new person - Assign the todo-item to the created person @@ -316,6 +292,7 @@ See the [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/t demonstrates how to use them. It uses local IDs to: - Create a new tag - Create a new person +- Update the person to clear an attribute (using built-in backing-store) - Create a new todo-item, tagged with the new tag, and owned by the new person - Assign the todo-item to the created person diff --git a/src/Examples/OpenApiKiotaClientExample/Worker.cs b/src/Examples/OpenApiKiotaClientExample/Worker.cs index 6a086eae00..074e17770b 100644 --- a/src/Examples/OpenApiKiotaClientExample/Worker.cs +++ b/src/Examples/OpenApiKiotaClientExample/Worker.cs @@ -38,7 +38,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) await SendOperationsRequestAsync(stoppingToken); - _ = await _apiClient.Api.People["999999"].GetAsync(cancellationToken: stoppingToken); + await _apiClient.Api.People["999999"].GetAsync(cancellationToken: stoppingToken); } catch (ErrorResponseDocument exception) { @@ -100,7 +100,7 @@ private async Task UpdatePersonAsync(CancellationToken cancellationToken) } }; - _ = await _apiClient.Api.People[updatePersonRequest.Data.Id].PatchAsync(updatePersonRequest, cancellationToken: cancellationToken); + await _apiClient.Api.People[updatePersonRequest.Data.Id].PatchAsync(updatePersonRequest, cancellationToken: cancellationToken); } private async Task SendOperationsRequestAsync(CancellationToken cancellationToken) @@ -131,7 +131,22 @@ private async Task SendOperationsRequestAsync(CancellationToken cancellationToke Lid = "new-person", Attributes = new AttributesInCreatePersonRequest { - LastName = "Cinderella" + FirstName = "Cinderella", + LastName = "Tremaine" + } + } + }, + new UpdatePersonOperation + { + Op = UpdateOperationCode.Update, + Data = new DataInUpdatePersonRequest + { + Type = PersonResourceType.People, + Lid = "new-person", + Attributes = new AttributesInUpdatePersonRequest + { + // The --backing-store switch enables to send null and default values. + FirstName = null } } }, @@ -191,7 +206,7 @@ private async Task SendOperationsRequestAsync(CancellationToken cancellationToke OperationsResponseDocument? operationsResponse = await _apiClient.Api.Operations.PostAsync(operationsRequest, cancellationToken: cancellationToken); - var newTodoItem = (TodoItemDataInResponse)operationsResponse!.AtomicResults!.ElementAt(2).Data!; + var newTodoItem = (TodoItemDataInResponse)operationsResponse!.AtomicResults!.ElementAt(3).Data!; Console.WriteLine($"Created todo-item with ID {newTodoItem.Id}: {newTodoItem.Attributes!.Description}."); } } diff --git a/src/Examples/OpenApiNSwagClientExample/ExampleApiClient.cs b/src/Examples/OpenApiNSwagClientExample/ExampleApiClient.cs deleted file mode 100644 index 3937bd915e..0000000000 --- a/src/Examples/OpenApiNSwagClientExample/ExampleApiClient.cs +++ /dev/null @@ -1,20 +0,0 @@ -using JetBrains.Annotations; -using JsonApiDotNetCore.OpenApi.Client.NSwag; -using Newtonsoft.Json; - -namespace OpenApiNSwagClientExample; - -[UsedImplicitly(ImplicitUseTargetFlags.Itself)] -public partial class ExampleApiClient : JsonApiClient -{ - partial void Initialize() - { - _instanceSettings = new JsonSerializerSettings(_settings.Value); - -#if DEBUG - _instanceSettings.Formatting = Formatting.Indented; -#endif - - SetSerializerSettingsForJsonApi(_instanceSettings); - } -} diff --git a/src/Examples/OpenApiNSwagClientExample/Worker.cs b/src/Examples/OpenApiNSwagClientExample/Worker.cs index 414cc34d09..3360eb7a8a 100644 --- a/src/Examples/OpenApiNSwagClientExample/Worker.cs +++ b/src/Examples/OpenApiNSwagClientExample/Worker.cs @@ -30,7 +30,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) await SendOperationsRequestAsync(stoppingToken); - _ = await _apiClient.GetPersonAsync("999999", null, null, stoppingToken); + await _apiClient.GetPersonAsync("999999", null, null, stoppingToken); } catch (ApiException exception) { @@ -57,20 +57,20 @@ private async Task UpdatePersonAsync(CancellationToken cancellationToken) Data = new DataInUpdatePersonRequest { Id = "1", - Attributes = new AttributesInUpdatePersonRequest + // Using TrackChangesFor to send "firstName: null" instead of omitting it. + Attributes = new TrackChangesFor(_apiClient) { - LastName = "Doe" - } + Initializer = + { + FirstName = null, + LastName = "Doe" + } + }.Initializer } }; - // This line results in sending "firstName: null" instead of omitting it. - using (_apiClient.WithPartialAttributeSerialization(updatePersonRequest, - person => person.FirstName)) - { - _ = await ApiResponse.TranslateAsync(async () => - await _apiClient.PatchPersonAsync(updatePersonRequest.Data.Id, updatePersonRequest, cancellationToken: cancellationToken)); - } + await ApiResponse.TranslateAsync(async () => + await _apiClient.PatchPersonAsync(updatePersonRequest.Data.Id, updatePersonRequest, cancellationToken: cancellationToken)); } private async Task SendOperationsRequestAsync(CancellationToken cancellationToken) @@ -97,10 +97,26 @@ private async Task SendOperationsRequestAsync(CancellationToken cancellationToke Lid = "new-person", Attributes = new AttributesInCreatePersonRequest { - LastName = "Cinderella" + FirstName = "Cinderella", + LastName = "Tremaine" } } }, + new UpdatePersonOperation + { + Data = new DataInUpdatePersonRequest + { + Lid = "new-person", + // Using TrackChangesFor to send "firstName: null" instead of omitting it. + Attributes = new TrackChangesFor(_apiClient) + { + Initializer = + { + FirstName = null + } + }.Initializer + } + }, new CreateTodoItemOperation { Data = new DataInCreateTodoItemRequest @@ -149,7 +165,7 @@ private async Task SendOperationsRequestAsync(CancellationToken cancellationToke ApiResponse operationsResponse = await _apiClient.PostOperationsAsync(operationsRequest, cancellationToken); - var newTodoItem = (TodoItemDataInResponse)operationsResponse.Result.Atomic_results.ElementAt(2).Data!; + var newTodoItem = (TodoItemDataInResponse)operationsResponse.Result.Atomic_results.ElementAt(3).Data!; Console.WriteLine($"Created todo-item with ID {newTodoItem.Id}: {newTodoItem.Attributes!.Description}."); } } diff --git a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/BlockedJsonInheritanceConverter.cs b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/BlockedJsonInheritanceConverter.cs new file mode 100644 index 0000000000..a3a7e627db --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/BlockedJsonInheritanceConverter.cs @@ -0,0 +1,43 @@ +using JetBrains.Annotations; +using Newtonsoft.Json; + +namespace JsonApiDotNetCore.OpenApi.Client.NSwag; + +// Referenced from liquid template, to ensure the built-in JsonInheritanceConverter from NSwag is never used. +[PublicAPI] +public abstract class BlockedJsonInheritanceConverter : JsonConverter +{ + private const string DefaultDiscriminatorName = "discriminator"; + + public string DiscriminatorName { get; } + + public override bool CanWrite => true; + public override bool CanRead => true; + + protected BlockedJsonInheritanceConverter() + : this(DefaultDiscriminatorName) + { + } + + protected BlockedJsonInheritanceConverter(string discriminatorName) + { + ArgumentException.ThrowIfNullOrEmpty(discriminatorName); + + DiscriminatorName = discriminatorName; + } + + public override bool CanConvert(Type objectType) + { + return true; + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + throw new InvalidOperationException("JsonInheritanceConverter is incompatible with JSON:API and must not be used."); + } + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + { + throw new InvalidOperationException("JsonInheritanceConverter is incompatible with JSON:API and must not be used."); + } +} diff --git a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/Build/JsonApiDotNetCore.OpenApi.Client.NSwag.props b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/Build/JsonApiDotNetCore.OpenApi.Client.NSwag.props index 879d38d152..cc65a6ac4a 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/Build/JsonApiDotNetCore.OpenApi.Client.NSwag.props +++ b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/Build/JsonApiDotNetCore.OpenApi.Client.NSwag.props @@ -8,5 +8,8 @@ true true true + JsonApiDotNetCore.OpenApi.Client.NSwag.JsonApiClient + Prism + $(MSBuildThisFileDirectory)../Templates diff --git a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/JsonApiClient.cs b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/JsonApiClient.cs index 72cbdce08a..14cf54825d 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/JsonApiClient.cs +++ b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/JsonApiClient.cs @@ -1,280 +1,363 @@ -using System.Diagnostics; -using System.Linq.Expressions; +using System.ComponentModel; using System.Reflection; +using JetBrains.Annotations; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; namespace JsonApiDotNetCore.OpenApi.Client.NSwag; /// -/// Base class to inherit auto-generated OpenAPI clients from. Provides support for partial POST/PATCH in JSON:API requests. +/// Base class to inherit auto-generated NSwag OpenAPI clients from. Provides support for partial POST/PATCH in JSON:API requests, optionally combined +/// with OpenAPI inheritance. /// -public abstract class JsonApiClient : IJsonApiClient +[PublicAPI] +public abstract class JsonApiClient { - private readonly DocumentJsonConverter _documentJsonConverter = new(); + private const string GeneratedJsonInheritanceConverterName = "JsonInheritanceConverter"; + private static readonly DefaultContractResolver UnmodifiedContractResolver = new(); + + private readonly Dictionary> _propertyStore = []; /// - /// Initial setup. Call this from the UpdateJsonSerializerSettings partial method in the auto-generated OpenAPI client. + /// Whether to automatically clear tracked properties after sending a request. Default value: true. Set to false to reuse tracked + /// properties for multiple requests and call after the last request to clean up. /// - protected void SetSerializerSettingsForJsonApi(JsonSerializerSettings settings) + public bool AutoClearTracked { get; set; } = true; + + internal void Track(T container) + where T : INotifyPropertyChanged, new() { - ArgumentNullException.ThrowIfNull(settings); + container.PropertyChanged += ContainerOnPropertyChanged; - settings.Converters.Add(_documentJsonConverter); + MarkAsTracked(container); } - /// - public IDisposable WithPartialAttributeSerialization(TRequestDocument requestDocument, - params Expression>[] alwaysIncludedAttributeSelectors) - where TRequestDocument : class + private void ContainerOnPropertyChanged(object? sender, PropertyChangedEventArgs args) { - ArgumentNullException.ThrowIfNull(requestDocument); - ArgumentNullException.ThrowIfNull(alwaysIncludedAttributeSelectors); - - HashSet attributeNames = []; - - foreach (Expression> selector in alwaysIncludedAttributeSelectors) + if (sender is INotifyPropertyChanged container && args.PropertyName != null) { - if (RemoveConvert(selector.Body) is MemberExpression selectorBody) - { - attributeNames.Add(selectorBody.Member.Name); - } - else - { - throw new ArgumentException($"The expression '{selector}' should select a single property. For example: 'article => article.Title'.", - nameof(alwaysIncludedAttributeSelectors)); - } + MarkAsTracked(container, args.PropertyName); } - - var alwaysIncludedAttributes = new AlwaysIncludedAttributes(attributeNames, typeof(TAttributesObject)); - _documentJsonConverter.RegisterDocument(requestDocument, alwaysIncludedAttributes); - - return new DocumentRegistrationScope(_documentJsonConverter, requestDocument); } - private static Expression RemoveConvert(Expression expression) + /// + /// Marks the specified properties on an object instance as tracked. Use this when unable to use inline initializer syntax for tracking. + /// + /// + /// The object instance whose properties to mark as tracked. + /// + /// + /// The names of the properties to mark as tracked. Properties in this list are always included. Any other property is only included if its value differs + /// from the property type's default value. + /// + public void MarkAsTracked(INotifyPropertyChanged container, params string[] propertyNames) { - Expression innerExpression = expression; + ArgumentNullException.ThrowIfNull(container); + ArgumentNullException.ThrowIfNull(propertyNames); - while (true) + if (!_propertyStore.TryGetValue(container, out ISet? properties)) { - if (innerExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression) - { - innerExpression = unaryExpression.Operand; - } - else - { - return innerExpression; - } + properties = new HashSet(); + _propertyStore[container] = properties; + } + + foreach (string propertyName in propertyNames) + { + properties.Add(propertyName); } } /// - /// Tracks a JSON:API attributes registration for a JSON:API document instance in the serializer. Disposing removes the registration, so the client can - /// be reused. + /// Clears all tracked properties. Call this after sending multiple requests when is set to false. /// - private sealed class DocumentRegistrationScope : IDisposable + public void ClearAllTracked() { - private readonly DocumentJsonConverter _documentJsonConverter; - private readonly object _document; - - public DocumentRegistrationScope(DocumentJsonConverter documentJsonConverter, object document) + foreach (INotifyPropertyChanged container in _propertyStore.Keys) { - ArgumentNullException.ThrowIfNull(documentJsonConverter); - ArgumentNullException.ThrowIfNull(document); - - _documentJsonConverter = documentJsonConverter; - _document = document; + container.PropertyChanged -= ContainerOnPropertyChanged; } - public void Dispose() - { - _documentJsonConverter.UnRegisterDocument(_document); - } + _propertyStore.Clear(); + } + + private void RemoveContainer(INotifyPropertyChanged container) + { + container.PropertyChanged -= ContainerOnPropertyChanged; + _propertyStore.Remove(container); } /// - /// Represents the set of JSON:API attributes to always send to the server, even if they are uninitialized (contain default value). + /// Initial setup. Call this from the Initialize partial method in the auto-generated NSwag client. /// - private sealed class AlwaysIncludedAttributes + /// + /// The to configure. + /// + /// + /// CAUTION: Calling this method makes the serializer stateful, which removes thread-safety of the owning auto-generated NSwag client. As a result, the + /// client MUST NOT be shared. So don't use a static instance, and don't register as a singleton in the service container. Also, do not execute parallel + /// requests on the same NSwag client instance. Executing multiple sequential requests on the same generated client instance is fine. + /// + protected void SetSerializerSettingsForJsonApi(JsonSerializerSettings serializerSettings) { - private readonly HashSet _propertyNames; - private readonly Type _attributesObjectType; + ArgumentNullException.ThrowIfNull(serializerSettings); - public AlwaysIncludedAttributes(HashSet propertyNames, Type attributesObjectType) - { - ArgumentNullException.ThrowIfNull(propertyNames); - ArgumentNullException.ThrowIfNull(attributesObjectType); + serializerSettings.ContractResolver = new InsertDiscriminatorPropertyContractResolver(); + serializerSettings.Converters.Insert(0, new PropertyTrackingInheritanceConverter(this)); + } - _propertyNames = propertyNames; - _attributesObjectType = attributesObjectType; - } + private static string? GetDiscriminatorName(Type objectType) + { + JsonContract contract = UnmodifiedContractResolver.ResolveContract(objectType); - public bool ContainsAttribute(string propertyName) + if (contract.Converter != null && contract.Converter.GetType().Name == GeneratedJsonInheritanceConverterName) { - return _propertyNames.Contains(propertyName); + var inheritanceConverter = (BlockedJsonInheritanceConverter)contract.Converter; + return inheritanceConverter.DiscriminatorName; } - public bool IsAttributesObjectType(Type type) - { - return _attributesObjectType == type; - } + return null; } /// - /// A that acts on JSON:API documents. + /// Replacement for the writing part of client-generated JsonInheritanceConverter that doesn't block other converters and preserves the JSON path on + /// error. /// - private sealed class DocumentJsonConverter : JsonConverter + private class InsertDiscriminatorPropertyContractResolver : DefaultContractResolver { - private readonly Dictionary _alwaysIncludedAttributesByDocument = []; - private readonly Dictionary> _documentsByType = []; - private bool _isSerializing; - - public override bool CanRead => false; - - public void RegisterDocument(object document, AlwaysIncludedAttributes alwaysIncludedAttributes) + protected override JsonObjectContract CreateObjectContract(Type objectType) { - _alwaysIncludedAttributesByDocument[document] = alwaysIncludedAttributes; + // NSwag adds [JsonConverter(typeof(JsonInheritanceConverter), "type")] on types to write the discriminator. + // This annotation has higher precedence over converters in the serializer settings, which is why ours normally won't execute. + // Once we tell Newtonsoft to ignore JsonInheritanceConverter, our converter can kick in. - Type documentType = document.GetType(); + JsonObjectContract contract = base.CreateObjectContract(objectType); - if (!_documentsByType.TryGetValue(documentType, out ISet? documents)) + if (contract.Converter != null && contract.Converter.GetType().Name == GeneratedJsonInheritanceConverterName) { - documents = new HashSet(); - _documentsByType[documentType] = documents; + contract.Converter = null; } - documents.Add(document); + return contract; } - public void UnRegisterDocument(object document) + protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) { - if (_alwaysIncludedAttributesByDocument.Remove(document)) - { - Type documentType = document.GetType(); - _documentsByType[documentType].Remove(document); + IList properties = base.CreateProperties(type, memberSerialization); - if (_documentsByType[documentType].Count == 0) - { - _documentsByType.Remove(documentType); - } - } - } - - public override bool CanConvert(Type objectType) - { - ArgumentNullException.ThrowIfNull(objectType); + string? discriminatorName = GetDiscriminatorName(type); - if (_isSerializing) + if (discriminatorName != null) { - // Protect against infinite recursion. - return false; + JsonProperty discriminatorProperty = CreateDiscriminatorProperty(discriminatorName, type); + properties.Insert(0, discriminatorProperty); } - return _documentsByType.ContainsKey(objectType); + return properties; } - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + private static JsonProperty CreateDiscriminatorProperty(string discriminatorName, Type declaringType) { - throw new UnreachableException(); + return new JsonProperty + { + PropertyName = discriminatorName, + PropertyType = typeof(string), + DeclaringType = declaringType, + ValueProvider = new DiscriminatorValueProvider(), + Readable = true, + Writable = true + }; } - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + private sealed class DiscriminatorValueProvider : IValueProvider { - ArgumentNullException.ThrowIfNull(writer); - ArgumentNullException.ThrowIfNull(serializer); - - if (value != null) + public object? GetValue(object target) { - if (_alwaysIncludedAttributesByDocument.TryGetValue(value, out AlwaysIncludedAttributes? alwaysIncludedAttributes)) - { - var attributesJsonConverter = new AttributesJsonConverter(alwaysIncludedAttributes); - serializer.Converters.Add(attributesJsonConverter); - } + Type type = target.GetType(); - try + foreach (Attribute attribute in type.GetCustomAttributes(true)) { - _isSerializing = true; - serializer.Serialize(writer, value); - } - finally - { - _isSerializing = false; + var shim = JsonInheritanceAttributeShim.TryCreate(attribute); + + if (shim != null && shim.Type == type) + { + return shim.Key; + } } + + return null; + } + + public void SetValue(object target, object? value) + { + // Nothing to do, NSwag doesn't generate a property for the discriminator. } } } /// - /// A that acts on JSON:API attribute objects. + /// Provides support for writing partial POST/PATCH in JSON:API requests via tracked properties. Provides reading of discriminator for inheritance. /// - private sealed class AttributesJsonConverter : JsonConverter + private sealed class PropertyTrackingInheritanceConverter : JsonConverter { - private readonly AlwaysIncludedAttributes _alwaysIncludedAttributes; - private bool _isSerializing; + [ThreadStatic] + private static bool _isWriting; - public override bool CanRead => false; + [ThreadStatic] + private static bool _isReading; - public AttributesJsonConverter(AlwaysIncludedAttributes alwaysIncludedAttributes) + private readonly JsonApiClient _apiClient; + + public override bool CanRead + { + get + { + if (_isReading) + { + // Prevent infinite recursion, but auto-reset so we'll participate in nested objects. + _isReading = false; + return false; + } + + return true; + } + } + + public override bool CanWrite { - ArgumentNullException.ThrowIfNull(alwaysIncludedAttributes); + get + { + if (_isWriting) + { + // Prevent infinite recursion, but auto-reset so we'll participate in nested objects. + _isWriting = false; + return false; + } - _alwaysIncludedAttributes = alwaysIncludedAttributes; + return true; + } + } + + public PropertyTrackingInheritanceConverter(JsonApiClient apiClient) + { + ArgumentNullException.ThrowIfNull(apiClient); + + _apiClient = apiClient; } public override bool CanConvert(Type objectType) { - ArgumentNullException.ThrowIfNull(objectType); + // Because this is called BEFORE CanRead/CanWrite, respond to both tracking and inheritance. + // We don't actually write for inheritance, so bail out later if that's the case. - if (_isSerializing) + if (_apiClient._propertyStore.Keys.Any(containingType => containingType.GetType() == objectType)) { - // Protect against infinite recursion. - return false; + return true; } - return _alwaysIncludedAttributes.IsAttributesObjectType(objectType); + var converterAttribute = objectType.GetCustomAttribute(true); + return converterAttribute is { ConverterType.Name: GeneratedJsonInheritanceConverterName }; } - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { - throw new UnreachableException(); + _isReading = true; + + try + { + JToken token = JToken.ReadFrom(reader); + string? discriminatorValue = GetDiscriminatorValue(objectType, token); + + Type resolvedType = ResolveTypeFromDiscriminatorValue(objectType, discriminatorValue); + return token.ToObject(resolvedType, serializer); + } + finally + { + _isReading = false; + } } - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + private static string? GetDiscriminatorValue(Type objectType, JToken token) { - ArgumentNullException.ThrowIfNull(writer); - ArgumentNullException.ThrowIfNull(serializer); + var jsonConverterAttribute = objectType.GetCustomAttribute(true)!; + + if (jsonConverterAttribute.ConverterParameters is not [string]) + { + throw new JsonException($"Expected single 'type' parameter for JsonInheritanceConverter usage on type '{objectType}'."); + } - if (value != null) + string discriminatorName = (string)jsonConverterAttribute.ConverterParameters[0]; + return token.Children().FirstOrDefault(property => property.Name == discriminatorName)?.Value.ToString(); + } + + private static Type ResolveTypeFromDiscriminatorValue(Type objectType, string? discriminatorValue) + { + if (discriminatorValue != null) { - if (_alwaysIncludedAttributes.IsAttributesObjectType(value.GetType())) + foreach (Attribute attribute in objectType.GetCustomAttributes(true)) { - AssertRequiredAttributesHaveNonDefaultValues(value, writer.Path); + var shim = JsonInheritanceAttributeShim.TryCreate(attribute); - serializer.ContractResolver = new JsonApiAttributeContractResolver(_alwaysIncludedAttributes); + if (shim != null && shim.Key == discriminatorValue) + { + return shim.Type; + } } + } + + return objectType; + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + _isWriting = true; - try + try + { + if (value is INotifyPropertyChanged container && _apiClient._propertyStore.TryGetValue(container, out ISet? properties)) { - _isSerializing = true; - serializer.Serialize(writer, value); + // Because we're overwriting NullValueHandling/DefaultValueHandling, we miss out on some validations that Newtonsoft would otherwise run. + AssertRequiredTrackedPropertiesHaveNoDefaultValue(container, properties, writer.Path); + + IContractResolver backupContractResolver = serializer.ContractResolver; + + try + { + // Caution: Swapping the contract resolver is not safe for concurrent usage, yet it needs to know the tracked instance. + serializer.ContractResolver = new PropertyTrackingContractResolver(container, properties); + serializer.Serialize(writer, value); + + if (_apiClient.AutoClearTracked) + { + _apiClient.RemoveContainer(container); + } + } + finally + { + serializer.ContractResolver = backupContractResolver; + } } - finally + else { - _isSerializing = false; + // We get here when the type is tracked, but not this instance. Or when writing for inheritance. + serializer.Serialize(writer, value); } } + finally + { + _isWriting = false; + } } - private void AssertRequiredAttributesHaveNonDefaultValues(object attributesObject, string jsonPath) + private static void AssertRequiredTrackedPropertiesHaveNoDefaultValue(object container, ISet properties, string jsonPath) { - foreach (PropertyInfo propertyInfo in attributesObject.GetType().GetProperties()) + foreach (PropertyInfo propertyInfo in container.GetType().GetProperties()) { - bool isExplicitlyIncluded = _alwaysIncludedAttributes.ContainsAttribute(propertyInfo.Name); + bool isTracked = properties.Contains(propertyInfo.Name); - if (!isExplicitlyIncluded) + if (!isTracked) { - AssertPropertyHasNonDefaultValueIfRequired(attributesObject, propertyInfo, jsonPath); + AssertPropertyHasNonDefaultValueIfRequired(container, propertyInfo, jsonPath); } } } @@ -285,12 +368,11 @@ private static void AssertPropertyHasNonDefaultValueIfRequired(object attributes if (jsonProperty is { Required: Required.Always or Required.AllowNull }) { - bool propertyHasDefaultValue = PropertyHasDefaultValue(propertyInfo, attributesObject); - - if (propertyHasDefaultValue) + if (PropertyHasDefaultValue(propertyInfo, attributesObject)) { - throw new InvalidOperationException( - $"Required property '{propertyInfo.Name}' at JSON path '{jsonPath}.{jsonProperty.PropertyName}' is not set. If sending its default value is intended, include it explicitly."); + throw new JsonSerializationException( + $"Cannot write a default value for property '{jsonProperty.PropertyName}'. Property requires a non-default value. Path '{jsonPath}'.", + jsonPath, 0, 0, null); } } } @@ -310,31 +392,30 @@ private static bool PropertyHasDefaultValue(PropertyInfo propertyInfo, object in } /// - /// Corrects the and JSON annotations at runtime, which appear on the auto-generated - /// properties for JSON:API attributes. For example: - /// - /// + /// Overrules the and annotations on generated properties for tracked object + /// instances to support JSON:API partial POST/PATCH. /// - private sealed class JsonApiAttributeContractResolver : DefaultContractResolver + private sealed class PropertyTrackingContractResolver : InsertDiscriminatorPropertyContractResolver { - private readonly AlwaysIncludedAttributes _alwaysIncludedAttributes; + private readonly INotifyPropertyChanged _container; + private readonly ISet _properties; - public JsonApiAttributeContractResolver(AlwaysIncludedAttributes alwaysIncludedAttributes) + public PropertyTrackingContractResolver(INotifyPropertyChanged container, ISet properties) { - ArgumentNullException.ThrowIfNull(alwaysIncludedAttributes); + ArgumentNullException.ThrowIfNull(container); + ArgumentNullException.ThrowIfNull(properties); - _alwaysIncludedAttributes = alwaysIncludedAttributes; + _container = container; + _properties = properties; } protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { JsonProperty jsonProperty = base.CreateProperty(member, memberSerialization); - if (_alwaysIncludedAttributes.IsAttributesObjectType(jsonProperty.DeclaringType!)) + if (jsonProperty.DeclaringType == _container.GetType()) { - if (_alwaysIncludedAttributes.ContainsAttribute(jsonProperty.UnderlyingName!)) + if (_properties.Contains(jsonProperty.UnderlyingName!)) { jsonProperty.NullValueHandling = NullValueHandling.Include; jsonProperty.DefaultValueHandling = DefaultValueHandling.Include; @@ -349,4 +430,29 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ return jsonProperty; } } + + private sealed class JsonInheritanceAttributeShim + { + private readonly Attribute _instance; + private readonly PropertyInfo _keyProperty; + private readonly PropertyInfo _typeProperty; + + public string Key => (string)_keyProperty.GetValue(_instance)!; + public Type Type => (Type)_typeProperty.GetValue(_instance)!; + + private JsonInheritanceAttributeShim(Attribute instance, Type type) + { + _instance = instance; + _keyProperty = type.GetProperty("Key") ?? throw new ArgumentException("Key property not found.", nameof(instance)); + _typeProperty = type.GetProperty("Type") ?? throw new ArgumentException("Type property not found.", nameof(instance)); + } + + public static JsonInheritanceAttributeShim? TryCreate(Attribute attribute) + { + ArgumentNullException.ThrowIfNull(attribute); + + Type type = attribute.GetType(); + return type.Name == "JsonInheritanceAttribute" ? new JsonInheritanceAttributeShim(attribute, type) : null; + } + } } diff --git a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/NotifyPropertySet.cs b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/NotifyPropertySet.cs new file mode 100644 index 0000000000..80adb56477 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/NotifyPropertySet.cs @@ -0,0 +1,72 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using JetBrains.Annotations; + +// Adapted from https://github.com/PrismLibrary/Prism/blob/9.0.537/src/Prism.Core/Mvvm/BindableBase.cs for JsonApiDotNetCore. + +#pragma warning disable AV1553 // Do not use optional parameters with default value null for strings, collections or tasks +#pragma warning disable AV1554 // Method contains optional parameter in type hierarchy +#pragma warning disable AV1562 // Do not declare a parameter as ref or out + +namespace JsonApiDotNetCore.OpenApi.Client.NSwag; + +/// +/// Implementation of that doesn't detect changes. +/// +[PublicAPI] +public abstract class NotifyPropertySet : INotifyPropertyChanged +{ + /// + /// Occurs when a property is set. + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Sets the property and notifies listeners. + /// + /// + /// Type of the property. + /// + /// + /// Reference to a property with both getter and setter. + /// + /// + /// Desired value for the property. + /// + /// + /// Name of the property used to notify listeners. This value is optional and can be provided automatically when invoked from compilers that support + /// CallerMemberName. + /// + /// + /// Always true. + /// + protected virtual bool SetProperty(ref T storage, T value, [CallerMemberName] string? propertyName = null) + { + storage = value; + RaisePropertyChanged(propertyName); + return true; + } + + /// + /// Raises this object's PropertyChanged event. + /// + /// + /// Name of the property used to notify listeners. This value is optional and can be provided automatically when invoked from compilers that support + /// . + /// + protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null) + { + OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); + } + + /// + /// Raises this object's PropertyChanged event. + /// + /// + /// The . + /// + protected virtual void OnPropertyChanged(PropertyChangedEventArgs args) + { + PropertyChanged?.Invoke(this, args); + } +} diff --git a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/Templates/Class.Inheritance.liquid b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/Templates/Class.Inheritance.liquid new file mode 100644 index 0000000000..1ddffc4af0 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/Templates/Class.Inheritance.liquid @@ -0,0 +1,14 @@ +{% comment %} + + Adapted from https://github.com/RicoSuter/NJsonSchema/blob/v11.1.0/src/NJsonSchema.CodeGeneration.CSharp/Templates/Class.Inheritance.liquid + for JsonApiDotNetCore, to intercept when properties are set. This is needed to support partial POST/PATCH to distinguish between sending + a property with its default value versus omitting the property. + +{% endcomment %} +{%- if RenderInpc %} +{% if HasInheritance %} : {{ BaseClassName }}{% else %} : System.ComponentModel.INotifyPropertyChanged{% endif %} +{%- elsif RenderPrism %} +{% if HasInheritance %} : {{ BaseClassName }}{% else %} : JsonApiDotNetCore.OpenApi.Client.NSwag.NotifyPropertySet{% endif %} +{%- else %} +{% if HasInheritance %} : {{ BaseClassName }}{% endif %} +{%- endif %} diff --git a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/Templates/Client.Class.Annotations.liquid b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/Templates/Client.Class.Annotations.liquid new file mode 100644 index 0000000000..bf791d968b --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/Templates/Client.Class.Annotations.liquid @@ -0,0 +1,14 @@ +{% comment %} + + Adapted from https://github.com/RicoSuter/NSwag/blob/v14.2.0/src/NSwag.CodeGeneration.CSharp/Templates/Client.Class.Annotations.liquid + for JsonApiDotNetCore, to initialize the JSON serializer for use with JSON:API. + +{% endcomment %} +partial class {{ Class }} +{ + partial void Initialize() + { + _instanceSettings = new Newtonsoft.Json.JsonSerializerSettings(_settings.Value); + SetSerializerSettingsForJsonApi(_instanceSettings); + } +} diff --git a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/Templates/JsonInheritanceConverter.liquid b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/Templates/JsonInheritanceConverter.liquid new file mode 100644 index 0000000000..e524f3e1f7 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/Templates/JsonInheritanceConverter.liquid @@ -0,0 +1,19 @@ +{% comment %} + + Adapted from https://github.com/RicoSuter/NJsonSchema/blob/v11.1.0/src/NJsonSchema.CodeGeneration.CSharp/Templates/JsonInheritanceConverter.liquid + for JsonApiDotNetCore, to provide alternate OpenAPI inheritance implementation that enables the use of third-party converters. + +{% endcomment %} +[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "{{ ToolchainVersion }}")] +public class JsonInheritanceConverter : JsonApiDotNetCore.OpenApi.Client.NSwag.BlockedJsonInheritanceConverter +{ + public JsonInheritanceConverter() + : base() + { + } + + public JsonInheritanceConverter(string discriminatorName) + : base(discriminatorName) + { + } +} diff --git a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/TrackChangesFor.cs b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/TrackChangesFor.cs new file mode 100644 index 0000000000..278b10ceac --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/TrackChangesFor.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; + +namespace JsonApiDotNetCore.OpenApi.Client.NSwag; + +/// +/// Tracks assignment of property values, to support JSON:API partial POST/PATCH. +/// +/// +/// The type whose property assignments to track. +/// +public sealed class TrackChangesFor + where T : INotifyPropertyChanged, new() +{ + public T Initializer { get; } + + public TrackChangesFor(JsonApiClient apiClient) + { + ArgumentNullException.ThrowIfNull(apiClient); + + Initializer = new T(); + apiClient.Track(Initializer); + } +} diff --git a/test/OpenApiNSwagClientTests/BaseOpenApiNSwagClientTests.cs b/test/OpenApiNSwagClientTests/BaseOpenApiNSwagClientTests.cs index 7d81e1592f..aa73b81824 100644 --- a/test/OpenApiNSwagClientTests/BaseOpenApiNSwagClientTests.cs +++ b/test/OpenApiNSwagClientTests/BaseOpenApiNSwagClientTests.cs @@ -1,24 +1,9 @@ -using System.Linq.Expressions; using System.Reflection; namespace OpenApiNSwagClientTests; public abstract class BaseOpenApiNSwagClientTests { - private const string AttributesObjectParameterName = "attributesObject"; - - protected static Expression> CreateAttributeSelectorFor(string propertyName) - where TAttributesObject : class - { - Type attributesObjectType = typeof(TAttributesObject); - - ParameterExpression parameter = Expression.Parameter(attributesObjectType, AttributesObjectParameterName); - MemberExpression property = Expression.Property(parameter, propertyName); - UnaryExpression castToObject = Expression.Convert(property, typeof(object)); - - return Expression.Lambda>(castToObject, parameter); - } - /// /// Sets the property on the specified source to its default value (null for string, 0 for int, false for bool, etc). /// diff --git a/test/OpenApiNSwagClientTests/ChangeTracking/SerializerChangeTrackingTests.cs b/test/OpenApiNSwagClientTests/ChangeTracking/SerializerChangeTrackingTests.cs new file mode 100644 index 0000000000..809ab9ed03 --- /dev/null +++ b/test/OpenApiNSwagClientTests/ChangeTracking/SerializerChangeTrackingTests.cs @@ -0,0 +1,830 @@ +using System.Net; +using FluentAssertions; +using JetBrains.Annotations; +using JsonApiDotNetCore.OpenApi.Client.NSwag; +using Newtonsoft.Json; +using OpenApiNSwagClientTests.NamingConventions.CamelCase.GeneratedCode; +using OpenApiNSwagClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOn.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiNSwagClientTests.ChangeTracking; + +public sealed class SerializerChangeTrackingTests +{ + [Fact] + public async Task Includes_properties_with_default_values_when_tracked() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + string resourceId = Unknown.StringId.Int32; + + var requestDocument = new UpdateResourceRequestDocument + { + Data = new DataInUpdateResourceRequest + { + Id = resourceId, + Attributes = new TrackChangesFor(apiClient) + { + Initializer = + { + ValueType = 0, + NullableValueType = null, + NullableReferenceType = null + } + }.Initializer + } + }; + + requestDocument.Data.Attributes.RequiredNonNullableReferenceType = "other"; + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(resourceId, null, requestDocument)); + + // Assert + wrapper.RequestBody.Should().BeJson($$""" + { + "data": { + "type": "resources", + "id": "{{resourceId}}", + "attributes": { + "requiredNonNullableReferenceType": "other", + "nullableReferenceType": null, + "valueType": 0, + "nullableValueType": null + } + } + } + """); + } + + [Fact] + public async Task Excludes_properties_with_default_values_when_not_tracked() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + string resourceId = Unknown.StringId.Int32; + + var requestDocument = new UpdateResourceRequestDocument + { + Data = new DataInUpdateResourceRequest + { + Id = resourceId, + Attributes = new TrackChangesFor(apiClient).Initializer + } + }; + + requestDocument.Data.Attributes.RequiredNonNullableReferenceType = "other"; + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(resourceId, null, requestDocument)); + + // Assert + wrapper.RequestBody.Should().BeJson($$""" + { + "data": { + "type": "resources", + "id": "{{resourceId}}", + "attributes": { + "requiredNonNullableReferenceType": "other" + } + } + } + """); + } + + [Fact] + public async Task Properties_can_be_changed_to_default_values_once_tracked() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + string resourceId = Unknown.StringId.Int32; + + var requestDocument = new UpdateResourceRequestDocument + { + Data = new DataInUpdateResourceRequest + { + Id = resourceId, + Attributes = new TrackChangesFor(apiClient) + { + Initializer = + { + ValueType = 1, + NullableValueType = 2, + NullableReferenceType = "other" + } + }.Initializer + } + }; + + requestDocument.Data.Attributes.ValueType = 0; + requestDocument.Data.Attributes.NullableValueType = null; + requestDocument.Data.Attributes.NullableReferenceType = null; + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(resourceId, null, requestDocument)); + + // Assert + wrapper.RequestBody.Should().BeJson($$""" + { + "data": { + "type": "resources", + "id": "{{resourceId}}", + "attributes": { + "nullableReferenceType": null, + "valueType": 0, + "nullableValueType": null + } + } + } + """); + } + + [Fact] + public async Task Automatically_clears_tracked_properties_after_sending_request() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + string resourceId = Unknown.StringId.Int32; + + var requestDocument = new UpdateResourceRequestDocument + { + Data = new DataInUpdateResourceRequest + { + Id = resourceId, + Attributes = new TrackChangesFor(apiClient) + { + Initializer = + { + ValueType = 0, + NullableValueType = null, + NullableReferenceType = null + } + }.Initializer + } + }; + + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(resourceId, null, requestDocument)); + wrapper.ChangeResponse(HttpStatusCode.NoContent, null); + + requestDocument.Data.Attributes.ValueType = 1; + requestDocument.Data.Attributes.RequiredValueType = 2; + requestDocument.Data.Attributes.RequiredNullableValueType = 3; + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(resourceId, null, requestDocument)); + + // Assert + wrapper.RequestBody.Should().BeJson($$""" + { + "data": { + "type": "resources", + "id": "{{resourceId}}", + "attributes": { + "valueType": 1, + "requiredValueType": 2, + "requiredNullableValueType": 3 + } + } + } + """); + } + + [Fact] + public async Task Can_preserve_tracked_properties_after_sending_request() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient) + { + AutoClearTracked = false + }; + + string resourceId = Unknown.StringId.Int32; + + var requestDocument = new UpdateResourceRequestDocument + { + Data = new DataInUpdateResourceRequest + { + Id = resourceId, + Attributes = new TrackChangesFor(apiClient) + { + Initializer = + { + ValueType = 0, + NullableValueType = null, + NullableReferenceType = null + } + }.Initializer + } + }; + + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(resourceId, null, requestDocument)); + wrapper.ChangeResponse(HttpStatusCode.NoContent, null); + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(resourceId, null, requestDocument)); + + // Assert + wrapper.RequestBody.Should().BeJson($$""" + { + "data": { + "type": "resources", + "id": "{{resourceId}}", + "attributes": { + "nullableReferenceType": null, + "valueType": 0, + "nullableValueType": null + } + } + } + """); + } + + [Fact] + public async Task Can_manually_clear_tracked_properties() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient) + { + AutoClearTracked = false + }; + + string resourceId = Unknown.StringId.Int32; + + var requestDocument = new UpdateResourceRequestDocument + { + Data = new DataInUpdateResourceRequest + { + Id = resourceId, + Attributes = new TrackChangesFor(apiClient) + { + Initializer = + { + ValueType = 0, + NullableValueType = null, + NullableReferenceType = null + } + }.Initializer + } + }; + + apiClient.ClearAllTracked(); + + requestDocument.Data.Attributes.ValueType = 1; + requestDocument.Data.Attributes.RequiredValueType = 2; + requestDocument.Data.Attributes.RequiredNullableValueType = 3; + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(resourceId, null, requestDocument)); + + // Assert + wrapper.RequestBody.Should().BeJson($$""" + { + "data": { + "type": "resources", + "id": "{{resourceId}}", + "attributes": { + "valueType": 1, + "requiredValueType": 2, + "requiredNullableValueType": 3 + } + } + } + """); + } + + [Fact] + public async Task Can_mark_existing_instance_as_tracked() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + string resourceId = Unknown.StringId.Int32; + + var requestDocument = new UpdateResourceRequestDocument + { + Data = new DataInUpdateResourceRequest + { + Id = resourceId, + Attributes = new AttributesInUpdateResourceRequest() + } + }; + + apiClient.MarkAsTracked(requestDocument.Data.Attributes); + requestDocument.Data.Attributes.RequiredNonNullableReferenceType = "other"; + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(resourceId, null, requestDocument)); + + // Assert + wrapper.RequestBody.Should().BeJson($$""" + { + "data": { + "type": "resources", + "id": "{{resourceId}}", + "attributes": { + "requiredNonNullableReferenceType": "other" + } + } + } + """); + } + + [Fact] + public async Task Can_mark_properties_on_existing_instance_as_tracked() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + string resourceId = Unknown.StringId.Int32; + + var requestDocument = new UpdateResourceRequestDocument + { + Data = new DataInUpdateResourceRequest + { + Id = resourceId, + Attributes = new AttributesInUpdateResourceRequest() + } + }; + + string[] propertyNamesToTrack = + [ + nameof(AttributesInUpdateResourceRequest.ValueType), + nameof(AttributesInUpdateResourceRequest.NullableValueType), + nameof(AttributesInUpdateResourceRequest.NullableReferenceType) + ]; + + apiClient.MarkAsTracked(requestDocument.Data.Attributes, propertyNamesToTrack); + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(resourceId, null, requestDocument)); + + // Assert + wrapper.RequestBody.Should().BeJson($$""" + { + "data": { + "type": "resources", + "id": "{{resourceId}}", + "attributes": { + "nullableReferenceType": null, + "valueType": 0, + "nullableValueType": null + } + } + } + """); + } + + [Fact] + public void Can_recursively_track_properties_on_complex_object() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new ExposeSerializerSettingsOnApiClient(wrapper.HttpClient); + + ComplexType complexObject = new TrackChangesFor(apiClient) + { + Initializer = + { + NullableDateTime = null, + NestedType = new TrackChangesFor(apiClient) + { + Initializer = + { + NullableInt = null, + NullableString = null + } + }.Initializer + } + }.Initializer; + + JsonSerializerSettings serializerSettings = apiClient.GetSerializerSettings(); + + // Act + string json = JsonConvert.SerializeObject(complexObject, serializerSettings); + + // Assert + json.Should().BeJson(""" + { + "NullableDateTime": null, + "NestedType": { + "NullableInt": null, + "NullableString": null + } + } + """); + } + + [Fact] + public async Task Tracking_a_different_instance_of_same_type_upfront_is_isolated() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + string resourceId = Unknown.StringId.Int32; + + _ = new UpdateResourceRequestDocument + { + Data = new DataInUpdateResourceRequest + { + Id = resourceId, + Attributes = new TrackChangesFor(apiClient) + { + Initializer = + { + ValueType = 0 + } + }.Initializer + } + }; + + var requestDocument = new UpdateResourceRequestDocument + { + Data = new DataInUpdateResourceRequest + { + Id = resourceId, + Attributes = new TrackChangesFor(apiClient) + { + Initializer = + { + NullableValueType = null, + NullableReferenceType = null + } + }.Initializer + } + }; + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(resourceId, null, requestDocument)); + + // Assert + wrapper.RequestBody.Should().BeJson($$""" + { + "data": { + "type": "resources", + "id": "{{resourceId}}", + "attributes": { + "nullableReferenceType": null, + "nullableValueType": null + } + } + } + """); + } + + [Fact] + public async Task Tracking_a_different_instance_of_same_type_afterward_is_isolated() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + string resourceId = Unknown.StringId.Int32; + + var requestDocument = new UpdateResourceRequestDocument + { + Data = new DataInUpdateResourceRequest + { + Id = resourceId, + Attributes = new TrackChangesFor(apiClient) + { + Initializer = + { + NullableValueType = null, + NullableReferenceType = null + } + }.Initializer + } + }; + + _ = new UpdateResourceRequestDocument + { + Data = new DataInUpdateResourceRequest + { + Id = resourceId, + Attributes = new TrackChangesFor(apiClient) + { + Initializer = + { + ValueType = 0 + } + }.Initializer + } + }; + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(resourceId, null, requestDocument)); + + // Assert + wrapper.RequestBody.Should().BeJson($$""" + { + "data": { + "type": "resources", + "id": "{{resourceId}}", + "attributes": { + "nullableReferenceType": null, + "nullableValueType": null + } + } + } + """); + } + + [Fact] + public async Task Can_reuse_api_client() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + string resourceId = Unknown.StringId.Int32; + + var requestDocument1 = new UpdateResourceRequestDocument + { + Data = new DataInUpdateResourceRequest + { + Id = resourceId, + Attributes = new TrackChangesFor(apiClient) + { + Initializer = + { + ValueType = 0 + } + }.Initializer + } + }; + + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(resourceId, null, requestDocument1)); + wrapper.ChangeResponse(HttpStatusCode.NoContent, null); + + var requestDocument2 = new UpdateResourceRequestDocument + { + Data = new DataInUpdateResourceRequest + { + Id = resourceId, + Attributes = new TrackChangesFor(apiClient) + { + Initializer = + { + NullableValueType = null, + NullableReferenceType = null + } + }.Initializer + } + }; + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(resourceId, null, requestDocument2)); + + // Assert + wrapper.RequestBody.Should().BeJson($$""" + { + "data": { + "type": "resources", + "id": "{{resourceId}}", + "attributes": { + "nullableReferenceType": null, + "nullableValueType": null + } + } + } + """); + } + + [Fact] + public async Task Can_reuse_request_document_on_same_api_client() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + string resourceId = Unknown.StringId.Int32; + + var requestDocument = new UpdateResourceRequestDocument + { + Data = new DataInUpdateResourceRequest + { + Id = resourceId, + Attributes = new TrackChangesFor(apiClient) + { + Initializer = + { + ValueType = 0, + RequiredNonNullableReferenceType = "first" + } + }.Initializer + } + }; + + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(resourceId, null, requestDocument)); + wrapper.ChangeResponse(HttpStatusCode.NoContent, null); + + requestDocument.Data.Attributes.NullableValueType = null; + requestDocument.Data.Attributes.NullableReferenceType = null; + requestDocument.Data.Attributes.RequiredNonNullableReferenceType = "other"; + + string[] propertyNamesToTrack = + [ + nameof(AttributesInUpdateResourceRequest.NullableValueType), + nameof(AttributesInUpdateResourceRequest.NullableReferenceType) + ]; + + apiClient.MarkAsTracked(requestDocument.Data.Attributes, propertyNamesToTrack); + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(resourceId, null, requestDocument)); + + // Assert + wrapper.RequestBody.Should().BeJson($$""" + { + "data": { + "type": "resources", + "id": "{{resourceId}}", + "attributes": { + "requiredNonNullableReferenceType": "other", + "nullableReferenceType": null, + "nullableValueType": null + } + } + } + """); + } + + [Fact] + public async Task Can_track_multiple_times_in_same_request_document() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new CamelCaseClient(wrapper.HttpClient); + + var requestDocument = new OperationsRequestDocument + { + Atomic_operations = + [ + new UpdateStaffMemberOperation + { + Data = new DataInUpdateStaffMemberRequest + { + Attributes = new TrackChangesFor(apiClient) + { + Initializer = + { + Age = null + } + }.Initializer + } + }, + new UpdateStaffMemberOperation + { + Data = new DataInUpdateStaffMemberRequest + { + Attributes = new TrackChangesFor(apiClient) + { + Initializer = + { + Name = "new-name" + } + }.Initializer + } + }, + new UpdateSupermarketOperation + { + Data = new DataInUpdateSupermarketRequest + { + Attributes = new TrackChangesFor(apiClient) + { + Initializer = + { + NameOfCity = "new-name-of-city" + } + }.Initializer + } + }, + new UpdateSupermarketOperation + { + Data = new DataInUpdateSupermarketRequest + { + Attributes = new TrackChangesFor(apiClient) + { + Initializer = + { + Kind = null + } + }.Initializer + } + } + ] + }; + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PostOperationsAsync(requestDocument)); + + // Assert + wrapper.RequestBody.Should().BeJson(""" + { + "atomic:operations": [ + { + "openapi:discriminator": "updateStaffMember", + "op": "update", + "data": { + "type": "staffMembers", + "attributes": { + "age": null + } + } + }, + { + "openapi:discriminator": "updateStaffMember", + "op": "update", + "data": { + "type": "staffMembers", + "attributes": { + "name": "new-name" + } + } + }, + { + "openapi:discriminator": "updateSupermarket", + "op": "update", + "data": { + "type": "supermarkets", + "attributes": { + "nameOfCity": "new-name-of-city" + } + } + }, + { + "openapi:discriminator": "updateSupermarket", + "op": "update", + "data": { + "type": "supermarkets", + "attributes": { + "kind": null + } + } + } + ] + } + """); + } + + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + private sealed class ComplexType : NotifyPropertySet + { + private DateTime? _nullableDateTime; + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public DateTime? NullableDateTime + { + get => _nullableDateTime; + set => SetProperty(ref _nullableDateTime, value); + } + + public NestedType? NestedType { get; set; } + } + + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + private sealed class NestedType : NotifyPropertySet + { + private int? _nullableInt; + private string? _nullableString; + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public int? NullableInt + { + get => _nullableInt; + set => SetProperty(ref _nullableInt, value); + } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore)] + public string? NullableString + { + get => _nullableString; + set => SetProperty(ref _nullableString, value); + } + } + + private sealed class ExposeSerializerSettingsOnApiClient(HttpClient httpClient) + : NrtOnMsvOnClient(httpClient) + { + public JsonSerializerSettings GetSerializerSettings() + { + return JsonSerializerSettings; + } + } +} diff --git a/test/OpenApiNSwagClientTests/LegacyOpenApi/GeneratedCode/LegacyClient.cs b/test/OpenApiNSwagClientTests/LegacyOpenApi/GeneratedCode/LegacyClient.cs deleted file mode 100644 index 2988c765b3..0000000000 --- a/test/OpenApiNSwagClientTests/LegacyOpenApi/GeneratedCode/LegacyClient.cs +++ /dev/null @@ -1,19 +0,0 @@ -using JsonApiDotNetCore.OpenApi.Client.NSwag; -using Newtonsoft.Json; - -#pragma warning disable CA1852 // Seal internal types - -namespace OpenApiNSwagClientTests.LegacyOpenApi.GeneratedCode; - -internal partial class LegacyClient : JsonApiClient -{ - partial void Initialize() - { - _instanceSettings = new JsonSerializerSettings(_settings.Value) - { - Formatting = Formatting.Indented - }; - - SetSerializerSettingsForJsonApi(_instanceSettings); - } -} diff --git a/test/OpenApiNSwagClientTests/LegacyOpenApi/PartialAttributeSerializationLifetimeTests.cs b/test/OpenApiNSwagClientTests/LegacyOpenApi/PartialAttributeSerializationLifetimeTests.cs deleted file mode 100644 index 9217f06dc3..0000000000 --- a/test/OpenApiNSwagClientTests/LegacyOpenApi/PartialAttributeSerializationLifetimeTests.cs +++ /dev/null @@ -1,444 +0,0 @@ -using System.Net; -using FluentAssertions; -using JsonApiDotNetCore.OpenApi.Client.NSwag; -using OpenApiNSwagClientTests.LegacyOpenApi.GeneratedCode; -using TestBuildingBlocks; -using Xunit; - -namespace OpenApiNSwagClientTests.LegacyOpenApi; - -public sealed class PartialAttributeSerializationLifetimeTests -{ - [Fact] - public async Task Disposed_registration_does_not_affect_request() - { - // Arrange - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - var apiClient = new LegacyClient(wrapper.HttpClient); - - const string airplaneId = "XUuiP"; - - var requestDocument = new UpdateAirplaneRequestDocument - { - Data = new DataInUpdateAirplaneRequest - { - Id = airplaneId, - Type = AirplaneResourceType.Airplanes, - Attributes = new AttributesInUpdateAirplaneRequest() - } - }; - - using (apiClient.WithPartialAttributeSerialization(requestDocument, - airplane => airplane.AirtimeInHours)) - { - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, null, requestDocument)); - } - - wrapper.ChangeResponse(HttpStatusCode.NoContent, null); - - // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, null, requestDocument)); - - // Assert - wrapper.RequestBody.Should().BeJson($$""" - { - "data": { - "type": "airplanes", - "id": "{{airplaneId}}", - "attributes": { - "is-in-maintenance": false - } - } - } - """); - } - - [Fact] - public async Task Registration_can_be_used_for_multiple_requests() - { - // Arrange - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - var apiClient = new LegacyClient(wrapper.HttpClient); - - const string airplaneId = "XUuiP"; - - var requestDocument = new UpdateAirplaneRequestDocument - { - Data = new DataInUpdateAirplaneRequest - { - Id = airplaneId, - Type = AirplaneResourceType.Airplanes, - Attributes = new AttributesInUpdateAirplaneRequest - { - AirtimeInHours = 100 - } - } - }; - - using (apiClient.WithPartialAttributeSerialization(requestDocument, - airplane => airplane.AirtimeInHours)) - { - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, null, requestDocument)); - - wrapper.ChangeResponse(HttpStatusCode.NoContent, null); - - requestDocument.Data.Attributes.AirtimeInHours = null; - - // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, null, requestDocument)); - } - - // Assert - wrapper.RequestBody.Should().BeJson($$""" - { - "data": { - "type": "airplanes", - "id": "{{airplaneId}}", - "attributes": { - "airtime-in-hours": null - } - } - } - """); - } - - [Fact] - public async Task Request_is_unaffected_by_registration_for_different_document_of_same_type() - { - // Arrange - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - var apiClient = new LegacyClient(wrapper.HttpClient); - - const string airplaneId1 = "XUuiP"; - - var requestDocument1 = new UpdateAirplaneRequestDocument - { - Data = new DataInUpdateAirplaneRequest - { - Id = airplaneId1, - Type = AirplaneResourceType.Airplanes, - Attributes = new AttributesInUpdateAirplaneRequest() - } - }; - - const string airplaneId2 = "DJy1u"; - - var requestDocument2 = new UpdateAirplaneRequestDocument - { - Data = new DataInUpdateAirplaneRequest - { - Id = airplaneId2, - Type = AirplaneResourceType.Airplanes, - Attributes = new AttributesInUpdateAirplaneRequest() - } - }; - - using (apiClient.WithPartialAttributeSerialization(requestDocument1, - airplane => airplane.AirtimeInHours)) - { - using (apiClient.WithPartialAttributeSerialization(requestDocument2, - airplane => airplane.SerialNumber)) - { - } - - // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId2, null, requestDocument2)); - } - - // Assert - wrapper.RequestBody.Should().BeJson($$""" - { - "data": { - "type": "airplanes", - "id": "{{airplaneId2}}", - "attributes": { - "is-in-maintenance": false - } - } - } - """); - } - - [Fact] - public async Task Attribute_values_can_be_changed_after_registration() - { - // Arrange - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - var apiClient = new LegacyClient(wrapper.HttpClient); - - const string airplaneId = "XUuiP"; - - var requestDocument = new UpdateAirplaneRequestDocument - { - Data = new DataInUpdateAirplaneRequest - { - Id = airplaneId, - Type = AirplaneResourceType.Airplanes, - Attributes = new AttributesInUpdateAirplaneRequest - { - IsInMaintenance = true - } - } - }; - - using (apiClient.WithPartialAttributeSerialization(requestDocument, - airplane => airplane.IsInMaintenance)) - { - requestDocument.Data.Attributes.IsInMaintenance = false; - - // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, null, requestDocument)); - } - - // Assert - wrapper.RequestBody.Should().BeJson($$""" - { - "data": { - "type": "airplanes", - "id": "{{airplaneId}}", - "attributes": { - "is-in-maintenance": false - } - } - } - """); - } - - [Fact] - public async Task Registration_is_unaffected_by_successive_registration_for_document_of_different_type() - { - // Arrange - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - var apiClient = new LegacyClient(wrapper.HttpClient); - - const string airplaneId1 = "XUuiP"; - - var requestDocument1 = new UpdateAirplaneRequestDocument - { - Data = new DataInUpdateAirplaneRequest - { - Id = airplaneId1, - Type = AirplaneResourceType.Airplanes, - Attributes = new AttributesInUpdateAirplaneRequest() - } - }; - - var requestDocument2 = new CreateAirplaneRequestDocument - { - Data = new DataInCreateAirplaneRequest - { - Type = AirplaneResourceType.Airplanes, - Attributes = new AttributesInCreateAirplaneRequest() - } - }; - - using (apiClient.WithPartialAttributeSerialization(requestDocument1, - airplane => airplane.IsInMaintenance)) - { - using (apiClient.WithPartialAttributeSerialization(requestDocument2, - airplane => airplane.AirtimeInHours)) - { - // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId1, null, requestDocument1)); - } - } - - // Assert - wrapper.RequestBody.Should().BeJson($$""" - { - "data": { - "type": "airplanes", - "id": "{{airplaneId1}}", - "attributes": { - "is-in-maintenance": false - } - } - } - """); - } - - [Fact] - public async Task Registration_is_unaffected_by_preceding_disposed_registration_for_different_document_of_same_type() - { - // Arrange - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - var apiClient = new LegacyClient(wrapper.HttpClient); - - const string airplaneId1 = "XUuiP"; - - var requestDocument1 = new UpdateAirplaneRequestDocument - { - Data = new DataInUpdateAirplaneRequest - { - Id = airplaneId1, - Type = AirplaneResourceType.Airplanes, - Attributes = new AttributesInUpdateAirplaneRequest() - } - }; - - using (apiClient.WithPartialAttributeSerialization(requestDocument1, - airplane => airplane.AirtimeInHours)) - { - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId1, null, requestDocument1)); - } - - const string airplaneId2 = "DJy1u"; - - var requestDocument2 = new UpdateAirplaneRequestDocument - { - Data = new DataInUpdateAirplaneRequest - { - Id = airplaneId2, - Type = AirplaneResourceType.Airplanes, - Attributes = new AttributesInUpdateAirplaneRequest - { - ManufacturedInCity = "Everett" - } - } - }; - - wrapper.ChangeResponse(HttpStatusCode.NoContent, null); - - using (apiClient.WithPartialAttributeSerialization(requestDocument2, - airplane => airplane.SerialNumber)) - { - // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId2, null, requestDocument2)); - } - - // Assert - wrapper.RequestBody.Should().BeJson($$""" - { - "data": { - "type": "airplanes", - "id": "{{airplaneId2}}", - "attributes": { - "serial-number": null, - "manufactured-in-city": "Everett" - } - } - } - """); - } - - [Fact] - public async Task Registration_is_unaffected_by_preceding_disposed_registration_for_document_of_different_type() - { - // Arrange - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - var apiClient = new LegacyClient(wrapper.HttpClient); - - var requestDocument1 = new CreateAirplaneRequestDocument - { - Data = new DataInCreateAirplaneRequest - { - Type = AirplaneResourceType.Airplanes, - Attributes = new AttributesInCreateAirplaneRequest - { - Name = "Jay Jay the Jet Plane" - } - } - }; - - using (apiClient.WithPartialAttributeSerialization(requestDocument1, - airplane => airplane.AirtimeInHours)) - { - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PostAirplaneAsync(null, requestDocument1)); - } - - const string airplaneId = "DJy1u"; - - var requestDocument2 = new UpdateAirplaneRequestDocument - { - Data = new DataInUpdateAirplaneRequest - { - Id = airplaneId, - Type = AirplaneResourceType.Airplanes, - Attributes = new AttributesInUpdateAirplaneRequest - { - ManufacturedInCity = "Everett" - } - } - }; - - wrapper.ChangeResponse(HttpStatusCode.NoContent, null); - - using (apiClient.WithPartialAttributeSerialization(requestDocument2, - airplane => airplane.SerialNumber)) - { - // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, null, requestDocument2)); - } - - // Assert - wrapper.RequestBody.Should().BeJson($$""" - { - "data": { - "type": "airplanes", - "id": "{{airplaneId}}", - "attributes": { - "serial-number": null, - "manufactured-in-city": "Everett" - } - } - } - """); - } - - [Fact] - public async Task Registration_is_unaffected_by_preceding_registration_for_different_document_of_same_type() - { - // Arrange - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - var apiClient = new LegacyClient(wrapper.HttpClient); - - const string airplaneId1 = "XUuiP"; - - var requestDocument1 = new UpdateAirplaneRequestDocument - { - Data = new DataInUpdateAirplaneRequest - { - Id = airplaneId1, - Type = AirplaneResourceType.Airplanes, - Attributes = new AttributesInUpdateAirplaneRequest() - } - }; - - const string airplaneId2 = "DJy1u"; - - var requestDocument2 = new UpdateAirplaneRequestDocument - { - Data = new DataInUpdateAirplaneRequest - { - Id = airplaneId2, - Type = AirplaneResourceType.Airplanes, - Attributes = new AttributesInUpdateAirplaneRequest() - } - }; - - using (apiClient.WithPartialAttributeSerialization(requestDocument1, - airplane => airplane.SerialNumber)) - { - using (apiClient.WithPartialAttributeSerialization(requestDocument2, - airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) - { - // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId2, null, requestDocument2)); - } - } - - // Assert - wrapper.RequestBody.Should().BeJson($$""" - { - "data": { - "type": "airplanes", - "id": "{{airplaneId2}}", - "attributes": { - "airtime-in-hours": null, - "is-in-maintenance": false - } - } - } - """); - } -} diff --git a/test/OpenApiNSwagClientTests/LegacyOpenApi/RequestTests.cs b/test/OpenApiNSwagClientTests/LegacyOpenApi/RequestTests.cs index 362cc995d4..b8622ed185 100644 --- a/test/OpenApiNSwagClientTests/LegacyOpenApi/RequestTests.cs +++ b/test/OpenApiNSwagClientTests/LegacyOpenApi/RequestTests.cs @@ -64,23 +64,20 @@ public async Task Partial_posting_resource_with_selected_relationships_produces_ { Data = new DataInCreateFlightRequest { - Type = FlightResourceType.Flights, Relationships = new RelationshipsInCreateFlightRequest { Purser = new ToOneFlightAttendantInRequest { Data = new FlightAttendantIdentifierInRequest { - Id = "bBJHu", - Type = FlightAttendantResourceType.FlightAttendants + Id = "bBJHu" } }, BackupPurser = new NullableToOneFlightAttendantInRequest { Data = new FlightAttendantIdentifierInRequest { - Id = "NInmX", - Type = FlightAttendantResourceType.FlightAttendants + Id = "NInmX" } } } @@ -146,21 +143,20 @@ public async Task Partial_posting_resource_produces_expected_request() { Data = new DataInCreateAirplaneRequest { - Type = AirplaneResourceType.Airplanes, - Attributes = new AttributesInCreateAirplaneRequest + Attributes = new TrackChangesFor(apiClient) { - Name = name, - AirtimeInHours = 800 - } + Initializer = + { + Name = name, + AirtimeInHours = 800, + SerialNumber = null + } + }.Initializer } }; - using (apiClient.WithPartialAttributeSerialization(requestDocument, - airplane => airplane.SerialNumber)) - { - // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PostAirplaneAsync(null, requestDocument)); - } + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PostAirplaneAsync(null, requestDocument)); // Assert wrapper.Request.ShouldNotBeNull(); @@ -200,20 +196,21 @@ public async Task Partial_patching_resource_produces_expected_request() Data = new DataInUpdateAirplaneRequest { Id = airplaneId, - Type = AirplaneResourceType.Airplanes, - Attributes = new AttributesInUpdateAirplaneRequest + Attributes = new TrackChangesFor(apiClient) { - LastServicedAt = lastServicedAt - } + Initializer = + { + LastServicedAt = lastServicedAt, + SerialNumber = null, + IsInMaintenance = false, + AirtimeInHours = null + } + }.Initializer } }; - using (apiClient.WithPartialAttributeSerialization(requestDocument, - airplane => airplane.SerialNumber, airplane => airplane.LastServicedAt, airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) - { - // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, null, requestDocument)); - } + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, null, requestDocument)); // Assert wrapper.Request.ShouldNotBeNull(); @@ -332,8 +329,7 @@ public async Task Patching_ToOne_relationship_produces_expected_request() { Data = new FlightAttendantIdentifierInRequest { - Id = "bBJHu", - Type = FlightAttendantResourceType.FlightAttendants + Id = "bBJHu" } }; @@ -393,12 +389,10 @@ public async Task Posting_ToMany_relationship_produces_expected_request() [ new FlightAttendantIdentifierInRequest { - Type = FlightAttendantResourceType.FlightAttendants, Id = "bBJHu" }, new FlightAttendantIdentifierInRequest { - Type = FlightAttendantResourceType.FlightAttendants, Id = "NInmX" } ] @@ -446,13 +440,11 @@ public async Task Patching_ToMany_relationship_produces_expected_request() [ new FlightAttendantIdentifierInRequest { - Id = "bBJHu", - Type = FlightAttendantResourceType.FlightAttendants + Id = "bBJHu" }, new FlightAttendantIdentifierInRequest { - Id = "NInmX", - Type = FlightAttendantResourceType.FlightAttendants + Id = "NInmX" } ] }; @@ -499,13 +491,11 @@ public async Task Deleting_ToMany_relationship_produces_expected_request() [ new FlightAttendantIdentifierInRequest { - Id = "bBJHu", - Type = FlightAttendantResourceType.FlightAttendants + Id = "bBJHu" }, new FlightAttendantIdentifierInRequest { - Id = "NInmX", - Type = FlightAttendantResourceType.FlightAttendants + Id = "NInmX" } ] }; diff --git a/test/OpenApiNSwagClientTests/LegacyOpenApi/ResponseTests.cs b/test/OpenApiNSwagClientTests/LegacyOpenApi/ResponseTests.cs index b20d586f0b..a4f3ae140e 100644 --- a/test/OpenApiNSwagClientTests/LegacyOpenApi/ResponseTests.cs +++ b/test/OpenApiNSwagClientTests/LegacyOpenApi/ResponseTests.cs @@ -299,15 +299,13 @@ public async Task Posting_resource_translates_response() { Data = new DataInCreateFlightRequest { - Type = FlightResourceType.Flights, Relationships = new RelationshipsInCreateFlightRequest { Purser = new ToOneFlightAttendantInRequest { Data = new FlightAttendantIdentifierInRequest { - Id = flightAttendantId, - Type = FlightAttendantResourceType.FlightAttendants + Id = flightAttendantId } } } @@ -355,8 +353,7 @@ public async Task Patching_resource_with_side_effects_translates_response() { Data = new DataInUpdateFlightRequest { - Id = flightId, - Type = FlightResourceType.Flights + Id = flightId } }; @@ -382,8 +379,7 @@ public async Task Patching_resource_without_side_effects_translates_response() { Data = new DataInUpdateFlightRequest { - Id = flightId, - Type = FlightResourceType.Flights + Id = flightId } })); @@ -590,8 +586,7 @@ public async Task Patching_ToOne_relationship_translates_response() { Data = new FlightAttendantIdentifierInRequest { - Id = "Adk2a", - Type = FlightAttendantResourceType.FlightAttendants + Id = "Adk2a" } }; @@ -652,13 +647,11 @@ public async Task Posting_ToMany_relationship_produces_empty_response() [ new FlightAttendantIdentifierInRequest { - Id = "Adk2a", - Type = FlightAttendantResourceType.FlightAttendants + Id = "Adk2a" }, new FlightAttendantIdentifierInRequest { - Id = "Un37k", - Type = FlightAttendantResourceType.FlightAttendants + Id = "Un37k" } ] }; @@ -683,13 +676,11 @@ public async Task Patching_ToMany_relationship_produces_empty_response() [ new FlightAttendantIdentifierInRequest { - Id = "Adk2a", - Type = FlightAttendantResourceType.FlightAttendants + Id = "Adk2a" }, new FlightAttendantIdentifierInRequest { - Id = "Un37k", - Type = FlightAttendantResourceType.FlightAttendants + Id = "Un37k" } ] }; @@ -714,13 +705,11 @@ public async Task Deleting_ToMany_relationship_produces_empty_response() [ new FlightAttendantIdentifierInRequest { - Id = "Adk2a", - Type = FlightAttendantResourceType.FlightAttendants + Id = "Adk2a" }, new FlightAttendantIdentifierInRequest { - Id = "Un37k", - Type = FlightAttendantResourceType.FlightAttendants + Id = "Un37k" } ] }; diff --git a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/CreateResourceTests.cs b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/CreateResourceTests.cs index e61e09bf53..3835ac2c80 100644 --- a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/CreateResourceTests.cs +++ b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/CreateResourceTests.cs @@ -1,4 +1,3 @@ -using System.Linq.Expressions; using System.Net; using System.Text.Json; using FluentAssertions; @@ -40,15 +39,11 @@ public async Task Can_set_attribute_to_default_value(string attributePropertyNam } }; - object? defaultValue = SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); - Expression> includeAttributeSelector = - CreateAttributeSelectorFor(attributePropertyName); - - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector); + object? defaultValue = SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes, attributePropertyName); // Act await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(null, requestDocument)); @@ -84,12 +79,11 @@ public async Task Can_omit_attribute(string attributePropertyName, string jsonPr } }; - SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes); // Act await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(null, requestDocument)); @@ -125,21 +119,20 @@ public async Task Cannot_omit_attribute(string attributePropertyName, string jso } }; - SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes); // Act Func action = async () => await apiClient.PostResourceAsync(null, requestDocument); // Assert - ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); assertion.Which.Message.Should().Be( - $"Required property '{attributePropertyName}' at JSON path 'data.attributes.{jsonPropertyName}' is not set. If sending its default value is intended, include it explicitly."); + $"Cannot write a default value for property '{jsonPropertyName}'. Property requires a non-default value. Path 'data.attributes'."); } [Theory] @@ -163,11 +156,12 @@ public async Task Can_clear_relationship(string relationshipPropertyName, string } }; - SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships, relationshipPropertyName); + // Act await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(null, requestDocument)); @@ -201,11 +195,12 @@ public async Task Cannot_clear_relationship(string relationshipPropertyName, str } }; - SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships, relationshipPropertyName); + // Act Func action = async () => await apiClient.PostResourceAsync(null, requestDocument); @@ -237,11 +232,12 @@ public async Task Can_omit_relationship(string relationshipPropertyName, string } }; - SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships); + // Act await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(null, requestDocument)); diff --git a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/GeneratedCode/NrtOffMsvOffClient.cs b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/GeneratedCode/NrtOffMsvOffClient.cs deleted file mode 100644 index 855e131b59..0000000000 --- a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/GeneratedCode/NrtOffMsvOffClient.cs +++ /dev/null @@ -1,19 +0,0 @@ -using JsonApiDotNetCore.OpenApi.Client.NSwag; -using Newtonsoft.Json; - -#pragma warning disable CA1852 // Seal internal types - -namespace OpenApiNSwagClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOff.GeneratedCode; - -internal partial class NrtOffMsvOffClient : JsonApiClient -{ - partial void Initialize() - { - _instanceSettings = new JsonSerializerSettings(_settings.Value) - { - Formatting = Formatting.Indented - }; - - SetSerializerSettingsForJsonApi(_instanceSettings); - } -} diff --git a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/UpdateResourceTests.cs b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/UpdateResourceTests.cs index b052846665..0abf4049bb 100644 --- a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/UpdateResourceTests.cs +++ b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOff/UpdateResourceTests.cs @@ -71,12 +71,11 @@ public async Task Can_omit_attribute(string attributePropertyName, string jsonPr } }; - SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes); // Act await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(requestDocument.Data.Id, null, requestDocument)); @@ -114,11 +113,12 @@ public async Task Can_omit_relationship(string relationshipPropertyName, string } }; - SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships); + // Act await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(requestDocument.Data.Id, null, requestDocument)); diff --git a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/CreateResourceTests.cs b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/CreateResourceTests.cs index 731287c44d..83c85ff779 100644 --- a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/CreateResourceTests.cs +++ b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/CreateResourceTests.cs @@ -1,4 +1,3 @@ -using System.Linq.Expressions; using System.Net; using System.Text.Json; using FluentAssertions; @@ -39,15 +38,11 @@ public async Task Can_set_attribute_to_default_value(string attributePropertyNam } }; - object? defaultValue = SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); - Expression> includeAttributeSelector = - CreateAttributeSelectorFor(attributePropertyName); - - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector); + object? defaultValue = SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes, attributePropertyName); // Act await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(null, requestDocument)); @@ -81,15 +76,11 @@ public async Task Cannot_set_attribute_to_default_value(string attributeProperty } }; - SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); - Expression> includeAttributeSelector = - CreateAttributeSelectorFor(attributePropertyName); - - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector); + SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes, attributePropertyName); // Act Func action = async () => await apiClient.PostResourceAsync(null, requestDocument); @@ -123,12 +114,11 @@ public async Task Can_omit_attribute(string attributePropertyName, string jsonPr } }; - SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes); // Act await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(null, requestDocument)); @@ -163,21 +153,20 @@ public async Task Cannot_omit_attribute(string attributePropertyName, string jso } }; - SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes); // Act Func action = async () => await apiClient.PostResourceAsync(null, requestDocument); // Assert - ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); assertion.Which.Message.Should().Be( - $"Required property '{attributePropertyName}' at JSON path 'data.attributes.{jsonPropertyName}' is not set. If sending its default value is intended, include it explicitly."); + $"Cannot write a default value for property '{jsonPropertyName}'. Property requires a non-default value. Path 'data.attributes'."); } [Theory] @@ -200,11 +189,12 @@ public async Task Can_clear_relationship(string relationshipPropertyName, string } }; - SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships, relationshipPropertyName); + // Act await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(null, requestDocument)); @@ -239,11 +229,12 @@ public async Task Cannot_clear_relationship(string relationshipPropertyName, str } }; - SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships, relationshipPropertyName); + // Act Func action = async () => await apiClient.PostResourceAsync(null, requestDocument); @@ -276,11 +267,12 @@ public async Task Can_omit_relationship(string relationshipPropertyName, string } }; - SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships); + // Act await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(null, requestDocument)); @@ -313,11 +305,12 @@ public async Task Cannot_omit_relationship(string relationshipPropertyName, stri } }; - SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships); + // Act Func action = async () => await apiClient.PostResourceAsync(null, requestDocument); diff --git a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/GeneratedCode/NrtOffMsvOnClient.cs b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/GeneratedCode/NrtOffMsvOnClient.cs deleted file mode 100644 index 676090a8b1..0000000000 --- a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/GeneratedCode/NrtOffMsvOnClient.cs +++ /dev/null @@ -1,19 +0,0 @@ -using JsonApiDotNetCore.OpenApi.Client.NSwag; -using Newtonsoft.Json; - -#pragma warning disable CA1852 // Seal internal types - -namespace OpenApiNSwagClientTests.ResourceFieldValidation.NullableReferenceTypesOff.ModelStateValidationOn.GeneratedCode; - -internal partial class NrtOffMsvOnClient : JsonApiClient -{ - partial void Initialize() - { - _instanceSettings = new JsonSerializerSettings(_settings.Value) - { - Formatting = Formatting.Indented - }; - - SetSerializerSettingsForJsonApi(_instanceSettings); - } -} diff --git a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/UpdateResourceTests.cs b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/UpdateResourceTests.cs index a6bebcbbe4..b9992c3d9a 100644 --- a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/UpdateResourceTests.cs +++ b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOff/ModelStateValidationOn/UpdateResourceTests.cs @@ -71,12 +71,11 @@ public async Task Can_omit_attribute(string attributePropertyName, string jsonPr } }; - SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes); // Act await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(requestDocument.Data.Id, null, requestDocument)); @@ -114,11 +113,12 @@ public async Task Can_omit_relationship(string relationshipPropertyName, string } }; - SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships); + // Act await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(requestDocument.Data.Id, null, requestDocument)); diff --git a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/CreateResourceTests.cs b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/CreateResourceTests.cs index c8c034e02b..4a8cf8c3cd 100644 --- a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/CreateResourceTests.cs +++ b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/CreateResourceTests.cs @@ -1,4 +1,3 @@ -using System.Linq.Expressions; using System.Net; using System.Text.Json; using FluentAssertions; @@ -42,15 +41,11 @@ public async Task Can_set_attribute_to_default_value(string attributePropertyNam } }; - object? defaultValue = SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); - Expression> includeAttributeSelector = - CreateAttributeSelectorFor(attributePropertyName); - - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector); + object? defaultValue = SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes, attributePropertyName); // Act await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(null, requestDocument)); @@ -87,15 +82,11 @@ public async Task Cannot_set_attribute_to_default_value(string attributeProperty } }; - SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); - Expression> includeAttributeSelector = - CreateAttributeSelectorFor(attributePropertyName); - - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector); + SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes, attributePropertyName); // Act Func action = async () => await apiClient.PostResourceAsync(null, requestDocument); @@ -132,12 +123,11 @@ public async Task Can_omit_attribute(string attributePropertyName, string jsonPr } }; - SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes); // Act await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(null, requestDocument)); @@ -176,21 +166,20 @@ public async Task Cannot_omit_attribute(string attributePropertyName, string jso } }; - SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes); // Act Func action = async () => await apiClient.PostResourceAsync(null, requestDocument); // Assert - ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); assertion.Which.Message.Should().Be( - $"Required property '{attributePropertyName}' at JSON path 'data.attributes.{jsonPropertyName}' is not set. If sending its default value is intended, include it explicitly."); + $"Cannot write a default value for property '{jsonPropertyName}'. Property requires a non-default value. Path 'data.attributes'."); } [Theory] @@ -216,11 +205,12 @@ public async Task Can_clear_relationship(string relationshipPropertyName, string } }; - SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships, relationshipPropertyName); + // Act await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(null, requestDocument)); @@ -258,11 +248,12 @@ public async Task Cannot_clear_relationship(string relationshipPropertyName, str } }; - SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships, relationshipPropertyName); + // Act Func action = async () => await apiClient.PostResourceAsync(null, requestDocument); @@ -297,11 +288,12 @@ public async Task Can_omit_relationship(string relationshipPropertyName, string } }; - SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships); + // Act await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(null, requestDocument)); @@ -336,11 +328,12 @@ public async Task Cannot_omit_relationship(string relationshipPropertyName, stri } }; - SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships); + // Act Func action = async () => await apiClient.PostResourceAsync(null, requestDocument); diff --git a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/GeneratedCode/NrtOnMsvOffClient.cs b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/GeneratedCode/NrtOnMsvOffClient.cs deleted file mode 100644 index b5f8a4e4b0..0000000000 --- a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/GeneratedCode/NrtOnMsvOffClient.cs +++ /dev/null @@ -1,19 +0,0 @@ -using JsonApiDotNetCore.OpenApi.Client.NSwag; -using Newtonsoft.Json; - -#pragma warning disable CA1852 // Seal internal types - -namespace OpenApiNSwagClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOff.GeneratedCode; - -internal partial class NrtOnMsvOffClient : JsonApiClient -{ - partial void Initialize() - { - _instanceSettings = new JsonSerializerSettings(_settings.Value) - { - Formatting = Formatting.Indented - }; - - SetSerializerSettingsForJsonApi(_instanceSettings); - } -} diff --git a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/UpdateResourceTests.cs b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/UpdateResourceTests.cs index cbd0cb7ff1..4337c42cdd 100644 --- a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/UpdateResourceTests.cs +++ b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOff/UpdateResourceTests.cs @@ -77,12 +77,11 @@ public async Task Can_omit_attribute(string attributePropertyName, string jsonPr } }; - SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes); // Act await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(requestDocument.Data.Id, null, requestDocument)); @@ -124,11 +123,12 @@ public async Task Can_omit_relationship(string relationshipPropertyName, string } }; - SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships); + // Act await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(requestDocument.Data.Id, null, requestDocument)); diff --git a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/CreateResourceTests.cs b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/CreateResourceTests.cs index dca43b71c8..c349962214 100644 --- a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/CreateResourceTests.cs +++ b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/CreateResourceTests.cs @@ -1,4 +1,3 @@ -using System.Linq.Expressions; using System.Net; using System.Text.Json; using FluentAssertions; @@ -41,15 +40,11 @@ public async Task Can_set_attribute_to_default_value(string attributePropertyNam } }; - object? defaultValue = SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); - Expression> includeAttributeSelector = - CreateAttributeSelectorFor(attributePropertyName); - - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector); + object? defaultValue = SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes, attributePropertyName); // Act await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(null, requestDocument)); @@ -87,15 +82,11 @@ public async Task Cannot_set_attribute_to_default_value(string attributeProperty } }; - SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); - Expression> includeAttributeSelector = - CreateAttributeSelectorFor(attributePropertyName); - - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector); + SetPropertyToDefaultValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes, attributePropertyName); // Act Func action = async () => await apiClient.PostResourceAsync(null, requestDocument); @@ -131,12 +122,11 @@ public async Task Can_omit_attribute(string attributePropertyName, string jsonPr } }; - SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes); // Act await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(null, requestDocument)); @@ -175,21 +165,20 @@ public async Task Cannot_omit_attribute(string attributePropertyName, string jso } }; - SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes); // Act Func action = async () => await apiClient.PostResourceAsync(null, requestDocument); // Assert - ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); assertion.Which.Message.Should().Be( - $"Required property '{attributePropertyName}' at JSON path 'data.attributes.{jsonPropertyName}' is not set. If sending its default value is intended, include it explicitly."); + $"Cannot write a default value for property '{jsonPropertyName}'. Property requires a non-default value. Path 'data.attributes'."); } [Theory] @@ -214,11 +203,12 @@ public async Task Can_clear_relationship(string relationshipPropertyName, string } }; - SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships, relationshipPropertyName); + // Act await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(null, requestDocument)); @@ -257,11 +247,12 @@ public async Task Cannot_clear_relationship(string relationshipPropertyName, str } }; - SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + SetDataPropertyToNull(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships, relationshipPropertyName); + // Act Func action = async () => await apiClient.PostResourceAsync(null, requestDocument); @@ -296,11 +287,12 @@ public async Task Can_omit_relationship(string relationshipPropertyName, string } }; - SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships); + // Act await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(null, requestDocument)); @@ -337,11 +329,12 @@ public async Task Cannot_omit_relationship(string relationshipPropertyName, stri } }; - SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships); + // Act Func action = async () => await apiClient.PostResourceAsync(null, requestDocument); diff --git a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/GeneratedCode/NrtOnMsvOnClient.cs b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/GeneratedCode/NrtOnMsvOnClient.cs deleted file mode 100644 index c6f65895ff..0000000000 --- a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/GeneratedCode/NrtOnMsvOnClient.cs +++ /dev/null @@ -1,19 +0,0 @@ -using JsonApiDotNetCore.OpenApi.Client.NSwag; -using Newtonsoft.Json; - -#pragma warning disable CA1852 // Seal internal types - -namespace OpenApiNSwagClientTests.ResourceFieldValidation.NullableReferenceTypesOn.ModelStateValidationOn.GeneratedCode; - -internal partial class NrtOnMsvOnClient : JsonApiClient -{ - partial void Initialize() - { - _instanceSettings = new JsonSerializerSettings(_settings.Value) - { - Formatting = Formatting.Indented - }; - - SetSerializerSettingsForJsonApi(_instanceSettings); - } -} diff --git a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/UpdateResourceTests.cs b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/UpdateResourceTests.cs index 03c37ca66b..6c0507bffd 100644 --- a/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/UpdateResourceTests.cs +++ b/test/OpenApiNSwagClientTests/ResourceFieldValidation/NullableReferenceTypesOn/ModelStateValidationOn/UpdateResourceTests.cs @@ -77,12 +77,11 @@ public async Task Can_omit_attribute(string attributePropertyName, string jsonPr } }; - SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); - using IDisposable _ = apiClient.WithPartialAttributeSerialization(requestDocument); + SetPropertyToInitialValue(requestDocument.Data.Attributes, attributePropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Attributes); // Act await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(requestDocument.Data.Id, null, requestDocument)); @@ -124,11 +123,12 @@ public async Task Can_omit_relationship(string relationshipPropertyName, string } }; - SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + SetPropertyToInitialValue(requestDocument.Data.Relationships, relationshipPropertyName); + apiClient.MarkAsTracked(requestDocument.Data.Relationships); + // Act await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(requestDocument.Data.Id, null, requestDocument)); diff --git a/test/OpenApiNSwagEndToEndTests/ModelStateValidation/ModelStateValidationTests.cs b/test/OpenApiNSwagEndToEndTests/ModelStateValidation/ModelStateValidationTests.cs index eda715af6a..7d9587304e 100644 --- a/test/OpenApiNSwagEndToEndTests/ModelStateValidation/ModelStateValidationTests.cs +++ b/test/OpenApiNSwagEndToEndTests/ModelStateValidation/ModelStateValidationTests.cs @@ -222,7 +222,6 @@ public async Task Cannot_exceed_min_length_constraint() { Data = new DataInCreateSocialMediaAccountRequest { - Type = SocialMediaAccountResourceType.SocialMediaAccounts, Attributes = new AttributesInCreateSocialMediaAccountRequest { LastName = newAccount.LastName, @@ -261,7 +260,6 @@ public async Task Cannot_exceed_max_length_constraint() { Data = new DataInCreateSocialMediaAccountRequest { - Type = SocialMediaAccountResourceType.SocialMediaAccounts, Attributes = new AttributesInCreateSocialMediaAccountRequest { LastName = newAccount.LastName,