json-api-dotnet / JsonApiDotNetCore

A framework for building JSON:API compliant REST APIs using ASP.NET and Entity Framework Core.
https://www.jsonapi.net
MIT License
662 stars 160 forks source link

OpenAPI support for Atomic Operations #1580

Closed bkoelman closed 1 week ago

bkoelman commented 1 week ago

This PR adds full support for using atomic operations with OpenAPI. Tests and examples using NSwag and Kiota are provided.

Various types have been renamed, which heavily affects generated clients. For example, personPatchRequestDocument is now updatePersonRequestDocument, and personAttributesInPostRequest has become attributesInCreatePersonRequest. These renames were done because atomic operations are reusing existing schemas. As we're breaking big now (sorry), I did some extra cleanups in schema names. The easiest way to overcome that is to use target-typed new.

Both id and lid are exposed in all request types, to avoid an explosion of schema types. But this only happens when an operations controller is found. The effect is that you can send a lid in a post-resource request (which will be rejected by the server). Because of the id/lid distinction, separate schemas are generated for resource identifiers in requests and responses (so staffMemberIdentifier is replaced by staffMemberIdentifierInRequest and staffMemberIdentifierInResponse).

Another change is that the id property has moved from the abstract dataInResponse type into the resource-specific derived types. The effect is that you first need to upcast/type-check before getting access to the id.

Both NSwag and Kiota choke on the quotes in the application/vnd.api+json; ext="https://jsonapi.org/ext/atomic" media type, generating code that doesn't compile. Therefore, OpenAPI uses the relaxed variant introduced in #1553.

The exposed operations in OpenAPI are based on the GenerateControllerEndpoints by default, but can be customized using your own IAtomicOperationFilter, a new feature that was introduced in #1561.

The generation of id/lid properties takes ClientIdGenerationMode into account for create-resource operations. Likewise, get-only or set-only attribute properties are hidden in the appropriate schemas.

There is currently no way to send null/default attribute values in an operations request with NSwag. This is tracked at #1577.

Example usage:

var operationsRequest = new OperationsRequestDocument
{
    Atomic_operations =
    [
        new CreateTagOperation
        {
            Data = new DataInCreateTagRequest
            {
                Lid = "new-tag",
                Attributes = new AttributesInCreateTagRequest
                {
                    Name = "Housekeeping"
                }
            }
        },
        new CreatePersonOperation
        {
            Data = new DataInCreatePersonRequest
            {
                Lid = "new-person",
                Attributes = new AttributesInCreatePersonRequest
                {
                    LastName = "Cinderella"
                }
            }
        },
        new CreateTodoItemOperation
        {
            Data = new DataInCreateTodoItemRequest
            {
                Lid = "new-todo-item",
                Attributes = new AttributesInCreateTodoItemRequest
                {
                    Description = "Put out the garbage",
                    Priority = TodoItemPriority.Medium
                },
                Relationships = new RelationshipsInCreateTodoItemRequest
                {
                    Owner = new ToOnePersonInRequest
                    {
                        Data = new PersonIdentifierInRequest
                        {
                            Lid = "new-person"
                        }
                    },
                    Tags = new ToManyTagInRequest
                    {
                        Data =
                        [
                            new TagIdentifierInRequest
                            {
                                Lid = "new-tag"
                            }
                        ]
                    }
                }
            }
        },
        new UpdateTodoItemAssigneeRelationshipOperation
        {
            Ref = new TodoItemAssigneeRelationshipIdentifier
            {
                Lid = "new-todo-item"
            },
            Data = new PersonIdentifierInRequest
            {
                Lid = "new-person"
            }
        }
    ]
};

ApiResponse<OperationsResponseDocument> operationsResponse =
    await _apiClient.PostOperationsAsync(operationsRequest, cancellationToken);

var newTodoItem = (TodoItemDataInResponse)operationsResponse.Result.Atomic_results.ElementAt(2).Data!;
Console.WriteLine($"Created todo-item with ID {newTodoItem.Id}: {newTodoItem.Attributes!.Description}.");

Closes #1060.

QUALITY CHECKLIST

codecov[bot] commented 1 week ago

Codecov Report

All modified and coverable lines are covered by tests :white_check_mark:

Project coverage is 91.39%. Comparing base (1053099) to head (e9476b8).

Additional details and impacted files ```diff @@ Coverage Diff @@ ## openapi #1580 +/- ## =========================================== + Coverage 91.27% 91.39% +0.11% =========================================== Files 398 412 +14 Lines 12905 13421 +516 Branches 2038 2095 +57 =========================================== + Hits 11779 12266 +487 - Misses 731 750 +19 - Partials 395 405 +10 ```

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.