From a257c08cf67a82518ec283e8f52288c83373fc32 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:12:53 +0100 Subject: [PATCH 1/3] Make CollectionConverter a singleton --- .../Repositories/DapperRepository.cs | 11 +++--- .../Repositories/ResourceChangeDetector.cs | 3 +- .../CollectionConverter.cs | 34 ++++++++++++++++--- .../Resources/Annotations/HasManyAttribute.cs | 6 ++-- .../Resources/Annotations/HasOneAttribute.cs | 2 +- .../Annotations/RelationshipAttribute.cs | 2 -- .../Processors/SetRelationshipProcessor.cs | 3 +- .../Errors/InvalidModelStateException.cs | 4 +-- .../Queries/QueryLayerComposer.cs | 3 +- .../QueryableBuilding/SelectClauseBuilder.cs | 5 ++- .../QueryableBuilding/WhereClauseBuilder.cs | 3 +- .../EntityFrameworkCoreRepository.cs | 21 ++++++------ .../Resources/OperationContainer.cs | 4 +-- .../Adapters/RelationshipDataAdapter.cs | 4 +-- .../Response/ResponseModelAdapter.cs | 4 +-- .../Services/JsonApiResourceService.cs | 11 +++--- 16 files changed, 64 insertions(+), 56 deletions(-) diff --git a/src/Examples/DapperExample/Repositories/DapperRepository.cs b/src/Examples/DapperExample/Repositories/DapperRepository.cs index 1da38697d9..7a46e69a53 100644 --- a/src/Examples/DapperExample/Repositories/DapperRepository.cs +++ b/src/Examples/DapperExample/Repositories/DapperRepository.cs @@ -103,7 +103,6 @@ public sealed partial class DapperRepository : IResourceReposito private readonly SqlCaptureStore _captureStore; private readonly ILoggerFactory _loggerFactory; private readonly ILogger> _logger; - private readonly CollectionConverter _collectionConverter = new(); private readonly ParameterFormatter _parameterFormatter = new(); private readonly DapperFacade _dapperFacade; @@ -270,12 +269,12 @@ private async Task ApplyTargetedFieldsAsync(TResource resourceFromRequest, TReso if (relationship is HasManyAttribute hasManyRelationship) { - HashSet rightResourceIds = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); + HashSet rightResourceIds = CollectionConverter.Instance.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); await _resourceDefinitionAccessor.OnSetToManyRelationshipAsync(leftResource, hasManyRelationship, rightResourceIds, writeOperation, cancellationToken); - return _collectionConverter.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType); + return CollectionConverter.Instance.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType); } return rightValue; @@ -464,7 +463,9 @@ public async Task AddToToManyRelationshipAsync(TResource? leftResource, [Disallo leftPlaceholderResource.Id = leftId; await _resourceDefinitionAccessor.OnAddToRelationshipAsync(leftPlaceholderResource, relationship, rightResourceIds, cancellationToken); - relationship.SetValue(leftPlaceholderResource, _collectionConverter.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType)); + + relationship.SetValue(leftPlaceholderResource, + CollectionConverter.Instance.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType)); await _resourceDefinitionAccessor.OnWritingAsync(leftPlaceholderResource, WriteOperationKind.AddToRelationship, cancellationToken); @@ -500,7 +501,7 @@ public async Task RemoveFromToManyRelationshipAsync(TResource leftResource, ISet var relationship = (HasManyAttribute)_targetedFields.Relationships.Single(); await _resourceDefinitionAccessor.OnRemoveFromRelationshipAsync(leftResource, relationship, rightResourceIds, cancellationToken); - relationship.SetValue(leftResource, _collectionConverter.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType)); + relationship.SetValue(leftResource, CollectionConverter.Instance.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType)); await _resourceDefinitionAccessor.OnWritingAsync(leftResource, WriteOperationKind.RemoveFromRelationship, cancellationToken); diff --git a/src/Examples/DapperExample/Repositories/ResourceChangeDetector.cs b/src/Examples/DapperExample/Repositories/ResourceChangeDetector.cs index 6b075d6cae..73213445e2 100644 --- a/src/Examples/DapperExample/Repositories/ResourceChangeDetector.cs +++ b/src/Examples/DapperExample/Repositories/ResourceChangeDetector.cs @@ -12,7 +12,6 @@ namespace DapperExample.Repositories; /// internal sealed class ResourceChangeDetector { - private readonly CollectionConverter _collectionConverter = new(); private readonly IDataModelService _dataModelService; private Dictionary _currentColumnValues = []; @@ -69,7 +68,7 @@ private Dictionary> CaptureRightRe foreach (RelationshipAttribute relationship in ResourceType.Relationships) { object? rightValue = relationship.GetValue(resource); - HashSet rightResources = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); + HashSet rightResources = CollectionConverter.Instance.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); relationshipValues[relationship] = rightResources; } diff --git a/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs b/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs index 750b896c27..f055c6548f 100644 --- a/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs +++ b/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs @@ -15,6 +15,12 @@ internal sealed class CollectionConverter typeof(IEnumerable<>) ]; + public static CollectionConverter Instance { get; } = new(); + + private CollectionConverter() + { + } + /// /// Creates a collection instance based on the specified collection type and copies the specified elements into it. /// @@ -22,7 +28,11 @@ internal sealed class CollectionConverter /// Source to copy from. /// /// - /// Target collection type, for example: typeof(List{Article}) or typeof(ISet{Person}). + /// Target collection type, for example: ) + /// ]]> or ) + /// ]]>. /// public IEnumerable CopyToTypedCollection(IEnumerable source, Type collectionType) { @@ -41,7 +51,12 @@ public IEnumerable CopyToTypedCollection(IEnumerable source, Type collectionType } /// - /// Returns a compatible collection type that can be instantiated, for example IList{Article} -> List{Article} or ISet{Article} -> HashSet{Article} + /// Returns a compatible collection type that can be instantiated, for example: -> List
+ /// ]]> or + /// -> HashSet
+ /// ]]>. ///
private Type ToConcreteCollectionType(Type collectionType) { @@ -80,7 +95,12 @@ public IReadOnlyCollection ExtractResources(object? value) } /// - /// Returns the element type if the specified type is a generic collection, for example: IList{string} -> string or IList -> null. + /// Returns the element type if the specified type is a generic collection, for example: -> string + /// ]]> or + /// null + /// ]]>. /// public Type? FindCollectionElementType(Type? type) { @@ -96,8 +116,12 @@ public IReadOnlyCollection ExtractResources(object? value) } /// - /// Indicates whether a instance can be assigned to the specified type, for example IList{Article} -> false or ISet{Article} -> - /// true. + /// Indicates whether a instance can be assigned to the specified type, for example: + /// -> false + /// ]]> or -> true + /// ]]>. /// public bool TypeCanContainHashSet(Type collectionType) { diff --git a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/HasManyAttribute.cs b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/HasManyAttribute.cs index 43161f99a8..274a6bb16f 100644 --- a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/HasManyAttribute.cs +++ b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/HasManyAttribute.cs @@ -59,7 +59,7 @@ private bool EvaluateIsManyToMany() { if (InverseNavigationProperty != null) { - Type? elementType = CollectionConverter.FindCollectionElementType(InverseNavigationProperty.PropertyType); + Type? elementType = CollectionConverter.Instance.FindCollectionElementType(InverseNavigationProperty.PropertyType); return elementType != null; } @@ -103,14 +103,14 @@ public void AddValue(object resource, IIdentifiable resourceToAdd) ArgumentGuard.NotNull(resourceToAdd); object? rightValue = GetValue(resource); - List rightResources = CollectionConverter.ExtractResources(rightValue).ToList(); + List rightResources = CollectionConverter.Instance.ExtractResources(rightValue).ToList(); if (!rightResources.Exists(nextResource => nextResource == resourceToAdd)) { rightResources.Add(resourceToAdd); Type collectionType = rightValue?.GetType() ?? Property.PropertyType; - IEnumerable typedCollection = CollectionConverter.CopyToTypedCollection(rightResources, collectionType); + IEnumerable typedCollection = CollectionConverter.Instance.CopyToTypedCollection(rightResources, collectionType); base.SetValue(resource, typedCollection); } } diff --git a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/HasOneAttribute.cs b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/HasOneAttribute.cs index 72212c76f2..51d22f9955 100644 --- a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/HasOneAttribute.cs +++ b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/HasOneAttribute.cs @@ -57,7 +57,7 @@ private bool EvaluateIsOneToOne() { if (InverseNavigationProperty != null) { - Type? elementType = CollectionConverter.FindCollectionElementType(InverseNavigationProperty.PropertyType); + Type? elementType = CollectionConverter.Instance.FindCollectionElementType(InverseNavigationProperty.PropertyType); return elementType == null; } diff --git a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs index 492af08c60..72bdecf7b0 100644 --- a/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs +++ b/src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs @@ -12,8 +12,6 @@ namespace JsonApiDotNetCore.Resources.Annotations; [PublicAPI] public abstract class RelationshipAttribute : ResourceFieldAttribute { - private protected static readonly CollectionConverter CollectionConverter = new(); - // This field is definitely assigned after building the resource graph, which is why its public equivalent is declared as non-nullable. private ResourceType? _rightType; diff --git a/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs b/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs index 3eeaa77fb3..abc40c4812 100644 --- a/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs +++ b/src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs @@ -10,7 +10,6 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors; public class SetRelationshipProcessor : ISetRelationshipProcessor where TResource : class, IIdentifiable { - private readonly CollectionConverter _collectionConverter = new(); private readonly ISetRelationshipService _service; public SetRelationshipProcessor(ISetRelationshipService service) @@ -40,7 +39,7 @@ public SetRelationshipProcessor(ISetRelationshipService service) if (relationship is HasManyAttribute) { - IReadOnlyCollection rightResources = _collectionConverter.ExtractResources(rightValue); + IReadOnlyCollection rightResources = CollectionConverter.Instance.ExtractResources(rightValue); return rightResources.ToHashSet(IdentifiableComparer.Instance); } diff --git a/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs index d55243e301..ccf23e1eb5 100644 --- a/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs +++ b/src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs @@ -319,8 +319,6 @@ private sealed class ArrayIndexerSegment( Func? getCollectionElementTypeCallback) : ModelStateKeySegment(modelType, isInComplexType, nextKey, sourcePointer, parent, getCollectionElementTypeCallback) { - private static readonly CollectionConverter CollectionConverter = new(); - public int ArrayIndex { get; } = arrayIndex; public Type GetCollectionElementType() @@ -333,7 +331,7 @@ private Type GetDeclaredCollectionElementType() { if (ModelType != typeof(string)) { - Type? elementType = CollectionConverter.FindCollectionElementType(ModelType); + Type? elementType = CollectionConverter.Instance.FindCollectionElementType(ModelType); if (elementType != null) { diff --git a/src/JsonApiDotNetCore/Queries/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/QueryLayerComposer.cs index 80125cde59..2abc476e92 100644 --- a/src/JsonApiDotNetCore/Queries/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/QueryLayerComposer.cs @@ -14,7 +14,6 @@ namespace JsonApiDotNetCore.Queries; [PublicAPI] public class QueryLayerComposer : IQueryLayerComposer { - private readonly CollectionConverter _collectionConverter = new(); private readonly IQueryConstraintProvider[] _constraintProviders; private readonly IResourceDefinitionAccessor _resourceDefinitionAccessor; private readonly IJsonApiOptions _options; @@ -413,7 +412,7 @@ public QueryLayer ComposeForUpdate([DisallowNull] TId id, ResourceType prim foreach (RelationshipAttribute relationship in _targetedFields.Relationships) { object? rightValue = relationship.GetValue(primaryResource); - HashSet rightResourceIds = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); + HashSet rightResourceIds = CollectionConverter.Instance.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); if (rightResourceIds.Count > 0) { diff --git a/src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs index 7e099fb091..3c3e46af38 100644 --- a/src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs @@ -14,7 +14,6 @@ public class SelectClauseBuilder : QueryClauseBuilder, ISelectClauseBuilder { private static readonly MethodInfo TypeGetTypeMethod = typeof(object).GetMethod("GetType")!; private static readonly MethodInfo TypeOpEqualityMethod = typeof(Type).GetMethod("op_Equality")!; - private static readonly CollectionConverter CollectionConverter = new(); private static readonly ConstantExpression NullConstant = Expression.Constant(null); private readonly IResourceFactory _resourceFactory; @@ -206,7 +205,7 @@ private MemberAssignment CreatePropertyAssignment(PropertySelector propertySelec private Expression CreateAssignmentRightHandSideForLayer(QueryLayer layer, LambdaScope outerLambdaScope, MemberExpression propertyAccess, PropertyInfo selectorPropertyInfo, QueryClauseBuilderContext context) { - Type? collectionElementType = CollectionConverter.FindCollectionElementType(selectorPropertyInfo.PropertyType); + Type? collectionElementType = CollectionConverter.Instance.FindCollectionElementType(selectorPropertyInfo.PropertyType); Type bodyElementType = collectionElementType ?? selectorPropertyInfo.PropertyType; if (collectionElementType != null) @@ -233,7 +232,7 @@ private static MethodCallExpression CreateCollectionInitializer(LambdaScope lamb Expression layerExpression = context.QueryableBuilder.ApplyQuery(layer, nestedContext); - string operationName = CollectionConverter.TypeCanContainHashSet(collectionProperty.PropertyType) ? "ToHashSet" : "ToList"; + string operationName = CollectionConverter.Instance.TypeCanContainHashSet(collectionProperty.PropertyType) ? "ToHashSet" : "ToList"; return CopyCollectionExtensionMethodCall(layerExpression, operationName, elementType); } diff --git a/src/JsonApiDotNetCore/Queries/QueryableBuilding/WhereClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/QueryableBuilding/WhereClauseBuilder.cs index a51aeefee7..efc9539c68 100644 --- a/src/JsonApiDotNetCore/Queries/QueryableBuilding/WhereClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/QueryableBuilding/WhereClauseBuilder.cs @@ -13,7 +13,6 @@ namespace JsonApiDotNetCore.Queries.QueryableBuilding; [PublicAPI] public class WhereClauseBuilder : QueryClauseBuilder, IWhereClauseBuilder { - private static readonly CollectionConverter CollectionConverter = new(); private static readonly ConstantExpression NullConstant = Expression.Constant(null); public virtual Expression ApplyWhere(FilterExpression filter, QueryClauseBuilderContext context) @@ -40,7 +39,7 @@ public override Expression VisitHas(HasExpression expression, QueryClauseBuilder { Expression property = Visit(expression.TargetCollection, context); - Type? elementType = CollectionConverter.FindCollectionElementType(property.Type); + Type? elementType = CollectionConverter.Instance.FindCollectionElementType(property.Type); if (elementType == null) { diff --git a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs index 9fc76bdefb..0dc43487da 100644 --- a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs +++ b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs @@ -31,7 +31,6 @@ namespace JsonApiDotNetCore.Repositories; public class EntityFrameworkCoreRepository : IResourceRepository, IRepositorySupportsTransaction where TResource : class, IIdentifiable { - private readonly CollectionConverter _collectionConverter = new(); private readonly ITargetedFields _targetedFields; private readonly DbContext _dbContext; private readonly IResourceGraph _resourceGraph; @@ -250,7 +249,7 @@ public virtual async Task CreateAsync(TResource resourceFromRequest, TResource r if (relationship is HasManyAttribute hasManyRelationship) { - HashSet rightResourceIds = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); + HashSet rightResourceIds = CollectionConverter.Instance.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); await _resourceDefinitionAccessor.OnSetToManyRelationshipAsync(leftResource, hasManyRelationship, rightResourceIds, writeOperation, cancellationToken); @@ -482,14 +481,14 @@ private ISet GetRightValueToStoreForAddToToMany(TResource leftRes object? rightValueStored = relationship.GetValue(leftResource); // @formatter:wrap_chained_method_calls chop_always - // @formatter:wrap_before_first_method_call true + // @formatter:wrap_after_property_in_chained_method_calls true - HashSet rightResourceIdsStored = _collectionConverter + HashSet rightResourceIdsStored = CollectionConverter.Instance .ExtractResources(rightValueStored) .Select(_dbContext.GetTrackedOrAttach) .ToHashSet(IdentifiableComparer.Instance); - // @formatter:wrap_before_first_method_call restore + // @formatter:wrap_after_property_in_chained_method_calls restore // @formatter:wrap_chained_method_calls restore if (rightResourceIdsStored.Count > 0) @@ -531,18 +530,18 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResour object? rightValueStored = relationship.GetValue(leftResourceTracked); // @formatter:wrap_chained_method_calls chop_always - // @formatter:wrap_before_first_method_call true + // @formatter:wrap_after_property_in_chained_method_calls true - IIdentifiable[] rightResourceIdsStored = _collectionConverter + IIdentifiable[] rightResourceIdsStored = CollectionConverter.Instance .ExtractResources(rightValueStored) .Concat(extraResourceIdsToRemove) .Select(_dbContext.GetTrackedOrAttach) .ToArray(); - // @formatter:wrap_before_first_method_call restore + // @formatter:wrap_after_property_in_chained_method_calls restore // @formatter:wrap_chained_method_calls restore - rightValueStored = _collectionConverter.CopyToTypedCollection(rightResourceIdsStored, relationship.Property.PropertyType); + rightValueStored = CollectionConverter.Instance.CopyToTypedCollection(rightResourceIdsStored, relationship.Property.PropertyType); relationship.SetValue(leftResourceTracked, rightValueStored); MarkRelationshipAsLoaded(leftResourceTracked, relationship); @@ -624,11 +623,11 @@ protected async Task UpdateRelationshipAsync(RelationshipAttribute relationship, return null; } - IReadOnlyCollection rightResources = _collectionConverter.ExtractResources(rightValue); + IReadOnlyCollection rightResources = CollectionConverter.Instance.ExtractResources(rightValue); IIdentifiable[] rightResourcesTracked = rightResources.Select(_dbContext.GetTrackedOrAttach).ToArray(); return rightValue is IEnumerable - ? _collectionConverter.CopyToTypedCollection(rightResourcesTracked, relationshipPropertyType) + ? CollectionConverter.Instance.CopyToTypedCollection(rightResourcesTracked, relationshipPropertyType) : rightResourcesTracked.Single(); } diff --git a/src/JsonApiDotNetCore/Resources/OperationContainer.cs b/src/JsonApiDotNetCore/Resources/OperationContainer.cs index 88ea29ecdc..2c8ec00d17 100644 --- a/src/JsonApiDotNetCore/Resources/OperationContainer.cs +++ b/src/JsonApiDotNetCore/Resources/OperationContainer.cs @@ -10,8 +10,6 @@ namespace JsonApiDotNetCore.Resources; [PublicAPI] public sealed class OperationContainer { - private static readonly CollectionConverter CollectionConverter = new(); - public IIdentifiable Resource { get; } public ITargetedFields TargetedFields { get; } public IJsonApiRequest Request { get; } @@ -56,7 +54,7 @@ public ISet GetSecondaryResources() private void AddSecondaryResources(RelationshipAttribute relationship, HashSet secondaryResources) { object? rightValue = relationship.GetValue(Resource); - IReadOnlyCollection rightResources = CollectionConverter.ExtractResources(rightValue); + IReadOnlyCollection rightResources = CollectionConverter.Instance.ExtractResources(rightValue); secondaryResources.UnionWith(rightResources); } diff --git a/src/JsonApiDotNetCore/Serialization/Request/Adapters/RelationshipDataAdapter.cs b/src/JsonApiDotNetCore/Serialization/Request/Adapters/RelationshipDataAdapter.cs index b0b230094d..02b3e80e43 100644 --- a/src/JsonApiDotNetCore/Serialization/Request/Adapters/RelationshipDataAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Request/Adapters/RelationshipDataAdapter.cs @@ -9,8 +9,6 @@ namespace JsonApiDotNetCore.Serialization.Request.Adapters; /// public sealed class RelationshipDataAdapter : BaseAdapter, IRelationshipDataAdapter { - private static readonly CollectionConverter CollectionConverter = new(); - private readonly IResourceIdentifierObjectAdapter _resourceIdentifierObjectAdapter; public RelationshipDataAdapter(IResourceIdentifierObjectAdapter resourceIdentifierObjectAdapter) @@ -109,7 +107,7 @@ private IEnumerable ConvertToManyRelationshipData(SingleOrManyData(IdentifiableComparer.Instance); diff --git a/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs b/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs index 0e4e605d96..3556eadafe 100644 --- a/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs +++ b/src/JsonApiDotNetCore/Serialization/Response/ResponseModelAdapter.cs @@ -18,8 +18,6 @@ namespace JsonApiDotNetCore.Serialization.Response; [PublicAPI] public class ResponseModelAdapter : IResponseModelAdapter { - private static readonly CollectionConverter CollectionConverter = new(); - private readonly IJsonApiRequest _request; private readonly IJsonApiOptions _options; private readonly ILinkBuilder _linkBuilder; @@ -304,7 +302,7 @@ private void TraverseRelationship(RelationshipAttribute relationship, IIdentifia } object? rightValue = effectiveRelationship.GetValue(leftResource); - IReadOnlyCollection rightResources = CollectionConverter.ExtractResources(rightValue); + IReadOnlyCollection rightResources = CollectionConverter.Instance.ExtractResources(rightValue); leftTreeNode.EnsureHasRelationship(effectiveRelationship); diff --git a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs index 2fc11684fc..bdaf89fbe0 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs @@ -21,7 +21,6 @@ namespace JsonApiDotNetCore.Services; public class JsonApiResourceService : IResourceService where TResource : class, IIdentifiable { - private readonly CollectionConverter _collectionConverter = new(); private readonly IResourceRepositoryAccessor _repositoryAccessor; private readonly IQueryLayerComposer _queryLayerComposer; private readonly IPaginationContext _paginationContext; @@ -280,7 +279,7 @@ private async Task ValidateResourcesToAssignInRelationshipsExistWithRefreshAsync if (!onlyIfTypeHierarchy || relationship.RightType.IsPartOfTypeHierarchy()) { object? rightValue = relationship.GetValue(primaryResource); - HashSet rightResourceIds = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); + HashSet rightResourceIds = CollectionConverter.Instance.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); if (rightResourceIds.Count > 0) { @@ -293,7 +292,7 @@ private async Task ValidateResourcesToAssignInRelationshipsExistWithRefreshAsync // Now that we've fetched them, update the request types so that resource definitions observe the actually stored types. object? newRightValue = relationship is HasOneAttribute ? rightResourceIds.FirstOrDefault() - : _collectionConverter.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType); + : CollectionConverter.Instance.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType); relationship.SetValue(primaryResource, newRightValue); } @@ -401,7 +400,7 @@ private async Task RemoveExistingIdsFromRelationshipRightSideAsync(Ha TResource leftResource = await GetForHasManyUpdateAsync(hasManyRelationship, leftId, rightResourceIds, cancellationToken); object? rightValue = hasManyRelationship.GetValue(leftResource); - IReadOnlyCollection existingRightResourceIds = _collectionConverter.ExtractResources(rightValue); + IReadOnlyCollection existingRightResourceIds = CollectionConverter.Instance.ExtractResources(rightValue); rightResourceIds.ExceptWith(existingRightResourceIds); @@ -422,7 +421,7 @@ private async Task GetForHasManyUpdateAsync(HasManyAttribute hasManyR { AssertRelationshipInJsonApiRequestIsNotNull(_request.Relationship); - HashSet rightResourceIds = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); + HashSet rightResourceIds = CollectionConverter.Instance.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance); object? newRightValue = rightValue; if (rightResourceIds.Count > 0) @@ -436,7 +435,7 @@ private async Task GetForHasManyUpdateAsync(HasManyAttribute hasManyR // Now that we've fetched them, update the request types so that resource definitions observe the actually stored types. newRightValue = _request.Relationship is HasOneAttribute ? rightResourceIds.FirstOrDefault() - : _collectionConverter.CopyToTypedCollection(rightResourceIds, _request.Relationship.Property.PropertyType); + : CollectionConverter.Instance.CopyToTypedCollection(rightResourceIds, _request.Relationship.Property.PropertyType); if (missingResources.Count > 0) { From 7bf6be825b482f4f3eb0927e3a609b07e2ca9b9b Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:17:34 +0100 Subject: [PATCH 2/3] To render selectors for derived types, a selector for each non-abstract derived type must be available in `QueryLayer.Selection`, which wasn't the case. Parsing of include chains is changed to include relationships on all concrete derived types. For example, given an inclusion chain of "wheels" at `/vehicles`, the parser now returns `Car.Wheels` and `Truck.Wheels`. This used to be `Vehicle.Wheels`. There's no point in adding abstract types, because they can't be instantiated in selectors. `QueryLayerComposer` was fixed to generate selectors for the left-type of these relationships, instead of for the `QueryLayer` resource type (so `Car.Wheels` and `Truck.Wheels` instead of `Vehicle.Wheels`). `SelectClauseBuilder` was missing a cast to the derived type when descending into relationship selectors, which is fixed now. Being unable to include relationships of sibling types after a POST/PATCH resource request at a base endpoint was because a `QueryLayer` for fetch-after-post was built against the accurized resource type, and then sent to the original (non-accurized) repository. For example, `POST /vehicles?include=TruckRelationship,CarRelationship` only works if the query executes against the non-accurized table (so `Vehicle` instead of `Car`, because `Car` doesn't contain `TruckRelationship`). The fix is to discard the accurized resource type and use the original `TResource`. Other improvements: - During the process of building expression trees, consistency checks have been added to prevent downstream crashes that are difficult to diagnose. - `SelectClauseBuilder` internally passed along a `LambdaScope` that overruled the one present in context, so care had to be taken to use the right one. To eliminate this pitfall, now a new context is forked which contains the appropriate `LambdaScope`, so the overruling parameter is no longer needed. Fixes #1639, fixes #1640. --- JsonApiDotNetCore.sln.DotSettings | 1 + .../Queries/Parsing/IncludeParser.cs | 28 +++++- .../Queries/QueryLayerComposer.cs | 2 +- .../QueryClauseBuilderContext.cs | 12 +++ .../QueryableBuilding/QueryableBuilder.cs | 10 ++ .../QueryableBuilding/SelectClauseBuilder.cs | 79 +++++++++------- .../IResourceRepositoryAccessor.cs | 5 + .../ResourceRepositoryAccessor.cs | 8 ++ .../Services/JsonApiResourceService.cs | 34 +++---- .../ResourceInheritanceReadTests.cs | 93 +++++++++++++++++++ .../ResourceInheritanceWriteTests.cs | 22 +++-- .../IncludeParseTests.cs | 4 +- 12 files changed, 236 insertions(+), 62 deletions(-) diff --git a/JsonApiDotNetCore.sln.DotSettings b/JsonApiDotNetCore.sln.DotSettings index 2a09eac0dc..5878341db6 100644 --- a/JsonApiDotNetCore.sln.DotSettings +++ b/JsonApiDotNetCore.sln.DotSettings @@ -668,6 +668,7 @@ $left$ = $right$; if ($argument$ is null) throw new ArgumentNullException(nameof($argument$)); WARNING True + True True True True diff --git a/src/JsonApiDotNetCore/Queries/Parsing/IncludeParser.cs b/src/JsonApiDotNetCore/Queries/Parsing/IncludeParser.cs index 99674b239d..6afa8a0698 100644 --- a/src/JsonApiDotNetCore/Queries/Parsing/IncludeParser.cs +++ b/src/JsonApiDotNetCore/Queries/Parsing/IncludeParser.cs @@ -122,7 +122,7 @@ private static ReadOnlyCollection LookupRelationshipName(string { // Depending on the left side of the include chain, we may match relationships anywhere in the resource type hierarchy. // This is compensated for when rendering the response, which substitutes relationships on base types with the derived ones. - IReadOnlySet relationships = parent.Relationship.RightType.GetRelationshipsInTypeOrDerived(relationshipName); + HashSet relationships = GetRelationshipsInConcreteTypes(parent.Relationship.RightType, relationshipName); if (relationships.Count > 0) { @@ -140,6 +140,32 @@ private static ReadOnlyCollection LookupRelationshipName(string return children.AsReadOnly(); } + private static HashSet GetRelationshipsInConcreteTypes(ResourceType resourceType, string relationshipName) + { + HashSet relationshipsToInclude = []; + + foreach (RelationshipAttribute relationship in resourceType.GetRelationshipsInTypeOrDerived(relationshipName)) + { + if (!relationship.LeftType.ClrType.IsAbstract) + { + relationshipsToInclude.Add(relationship); + } + + IncludeRelationshipsFromConcreteDerivedTypes(relationship, relationshipsToInclude); + } + + return relationshipsToInclude; + } + + private static void IncludeRelationshipsFromConcreteDerivedTypes(RelationshipAttribute relationship, HashSet relationshipsToInclude) + { + foreach (ResourceType derivedType in relationship.LeftType.GetAllConcreteDerivedTypes()) + { + RelationshipAttribute relationshipInDerived = derivedType.GetRelationshipByPublicName(relationship.PublicName); + relationshipsToInclude.Add(relationshipInDerived); + } + } + private static void AssertRelationshipsFound(HashSet relationshipsFound, string relationshipName, IReadOnlyCollection parents, int position) { diff --git a/src/JsonApiDotNetCore/Queries/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/QueryLayerComposer.cs index 2abc476e92..1804027de4 100644 --- a/src/JsonApiDotNetCore/Queries/QueryLayerComposer.cs +++ b/src/JsonApiDotNetCore/Queries/QueryLayerComposer.cs @@ -212,7 +212,7 @@ private IImmutableSet ProcessIncludeSet(IImmutableSet< foreach (IncludeElementExpression includeElement in includeElementsEvaluated) { parentLayer.Selection ??= new FieldSelection(); - FieldSelectors selectors = parentLayer.Selection.GetOrCreateSelectors(parentLayer.ResourceType); + FieldSelectors selectors = parentLayer.Selection.GetOrCreateSelectors(includeElement.Relationship.LeftType); if (!selectors.ContainsField(includeElement.Relationship)) { diff --git a/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryClauseBuilderContext.cs b/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryClauseBuilderContext.cs index 05cccf7943..b218b09ca6 100644 --- a/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryClauseBuilderContext.cs +++ b/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryClauseBuilderContext.cs @@ -61,6 +61,7 @@ public QueryClauseBuilderContext(Expression source, ResourceType resourceType, T ArgumentGuard.NotNull(lambdaScopeFactory); ArgumentGuard.NotNull(lambdaScope); ArgumentGuard.NotNull(queryableBuilder); + AssertSameType(source.Type, resourceType); Source = source; ResourceType = resourceType; @@ -72,6 +73,17 @@ public QueryClauseBuilderContext(Expression source, ResourceType resourceType, T State = state; } + private static void AssertSameType(Type sourceType, ResourceType resourceType) + { + Type? sourceElementType = CollectionConverter.Instance.FindCollectionElementType(sourceType); + + if (sourceElementType != resourceType.ClrType) + { + throw new InvalidOperationException( + $"Internal error: Mismatch between expression type '{sourceElementType?.Name}' and resource type '{resourceType.ClrType.Name}'."); + } + } + public QueryClauseBuilderContext WithSource(Expression source) { ArgumentGuard.NotNull(source); diff --git a/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryableBuilder.cs b/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryableBuilder.cs index 6b52b3d69b..c04eb68820 100644 --- a/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryableBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryableBuilder.cs @@ -35,6 +35,7 @@ public virtual Expression ApplyQuery(QueryLayer layer, QueryableBuilderContext c { ArgumentGuard.NotNull(layer); ArgumentGuard.NotNull(context); + AssertSameType(layer.ResourceType, context.ElementType); Expression expression = context.Source; @@ -66,6 +67,15 @@ public virtual Expression ApplyQuery(QueryLayer layer, QueryableBuilderContext c return expression; } + private static void AssertSameType(ResourceType resourceType, Type elementType) + { + if (elementType != resourceType.ClrType) + { + throw new InvalidOperationException( + $"Internal error: Mismatch between query layer type '{resourceType.ClrType.Name}' and query element type '{elementType.Name}'."); + } + } + protected virtual Expression ApplyInclude(Expression source, IncludeExpression include, ResourceType resourceType, QueryableBuilderContext context) { ArgumentGuard.NotNull(source); diff --git a/src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs index 3c3e46af38..9d2498620b 100644 --- a/src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs +++ b/src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs @@ -29,36 +29,47 @@ public virtual Expression ApplySelect(FieldSelection selection, QueryClauseBuild { ArgumentGuard.NotNull(selection); - Expression bodyInitializer = CreateLambdaBodyInitializer(selection, context.ResourceType, context.LambdaScope, false, context); + Expression bodyInitializer = CreateLambdaBodyInitializer(selection, context.ResourceType, false, context); LambdaExpression lambda = Expression.Lambda(bodyInitializer, context.LambdaScope.Parameter); return SelectExtensionMethodCall(context.ExtensionType, context.Source, context.LambdaScope.Parameter.Type, lambda); } - private Expression CreateLambdaBodyInitializer(FieldSelection selection, ResourceType resourceType, LambdaScope lambdaScope, - bool lambdaAccessorRequiresTestForNull, QueryClauseBuilderContext context) + private Expression CreateLambdaBodyInitializer(FieldSelection selection, ResourceType resourceType, bool lambdaAccessorRequiresTestForNull, + QueryClauseBuilderContext context) { + AssertSameType(context.LambdaScope.Accessor.Type, resourceType); + IReadOnlyEntityType entityType = context.EntityModel.FindEntityType(resourceType.ClrType)!; IReadOnlyEntityType[] concreteEntityTypes = entityType.GetConcreteDerivedTypesInclusive().ToArray(); Expression bodyInitializer = concreteEntityTypes.Length > 1 - ? CreateLambdaBodyInitializerForTypeHierarchy(selection, resourceType, concreteEntityTypes, lambdaScope, context) - : CreateLambdaBodyInitializerForSingleType(selection, resourceType, lambdaScope, context); + ? CreateLambdaBodyInitializerForTypeHierarchy(selection, resourceType, concreteEntityTypes, context) + : CreateLambdaBodyInitializerForSingleType(selection, resourceType, context); if (!lambdaAccessorRequiresTestForNull) { return bodyInitializer; } - return TestForNull(lambdaScope.Accessor, bodyInitializer); + return TestForNull(context.LambdaScope.Accessor, bodyInitializer); + } + + private static void AssertSameType(Type lambdaAccessorType, ResourceType resourceType) + { + if (lambdaAccessorType != resourceType.ClrType) + { + throw new InvalidOperationException( + $"Internal error: Mismatch between lambda accessor type '{lambdaAccessorType.Name}' and resource type '{resourceType.ClrType.Name}'."); + } } private Expression CreateLambdaBodyInitializerForTypeHierarchy(FieldSelection selection, ResourceType baseResourceType, - IEnumerable concreteEntityTypes, LambdaScope lambdaScope, QueryClauseBuilderContext context) + IEnumerable concreteEntityTypes, QueryClauseBuilderContext context) { IReadOnlySet resourceTypes = selection.GetResourceTypes(); - Expression rootCondition = lambdaScope.Accessor; + Expression rootCondition = context.LambdaScope.Accessor; foreach (IReadOnlyEntityType entityType in concreteEntityTypes) { @@ -73,14 +84,14 @@ private Expression CreateLambdaBodyInitializerForTypeHierarchy(FieldSelection se Dictionary.ValueCollection propertySelectors = ToPropertySelectors(fieldSelectors, resourceType, entityType.ClrType, context.EntityModel); - MemberBinding[] propertyAssignments = propertySelectors.Select(selector => CreatePropertyAssignment(selector, lambdaScope, context)) + MemberBinding[] propertyAssignments = propertySelectors.Select(selector => CreatePropertyAssignment(selector, context)) .Cast().ToArray(); NewExpression createInstance = _resourceFactory.CreateNewExpression(entityType.ClrType); MemberInitExpression memberInit = Expression.MemberInit(createInstance, propertyAssignments); UnaryExpression castToBaseType = Expression.Convert(memberInit, baseResourceType.ClrType); - BinaryExpression typeCheck = CreateRuntimeTypeCheck(lambdaScope, entityType.ClrType); + BinaryExpression typeCheck = CreateRuntimeTypeCheck(context.LambdaScope, entityType.ClrType); rootCondition = Expression.Condition(typeCheck, castToBaseType, rootCondition); } } @@ -100,18 +111,16 @@ private static BinaryExpression CreateRuntimeTypeCheck(LambdaScope lambdaScope, return Expression.MakeBinary(ExpressionType.Equal, getTypeCall, concreteTypeConstant, false, TypeOpEqualityMethod); } - private MemberInitExpression CreateLambdaBodyInitializerForSingleType(FieldSelection selection, ResourceType resourceType, LambdaScope lambdaScope, + private MemberInitExpression CreateLambdaBodyInitializerForSingleType(FieldSelection selection, ResourceType resourceType, QueryClauseBuilderContext context) { FieldSelectors fieldSelectors = selection.GetOrCreateSelectors(resourceType); Dictionary.ValueCollection propertySelectors = - ToPropertySelectors(fieldSelectors, resourceType, lambdaScope.Accessor.Type, context.EntityModel); - - MemberBinding[] propertyAssignments = propertySelectors.Select(selector => CreatePropertyAssignment(selector, lambdaScope, context)) - .Cast().ToArray(); + ToPropertySelectors(fieldSelectors, resourceType, context.LambdaScope.Accessor.Type, context.EntityModel); - NewExpression createInstance = _resourceFactory.CreateNewExpression(lambdaScope.Accessor.Type); + MemberBinding[] propertyAssignments = propertySelectors.Select(selector => CreatePropertyAssignment(selector, context)).Cast().ToArray(); + NewExpression createInstance = _resourceFactory.CreateNewExpression(context.LambdaScope.Accessor.Type); return Expression.MemberInit(createInstance, propertyAssignments); } @@ -182,35 +191,40 @@ private static void IncludeEagerLoads(ResourceType resourceType, Dictionary public interface IResourceRepositoryAccessor { + /// + /// Uses the to lookup the corresponding for the specified CLR type. + /// + ResourceType LookupResourceType(Type resourceClrType); + /// /// Invokes for the specified resource type. /// diff --git a/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs b/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs index 23d03eb9dd..827c2259fd 100644 --- a/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs +++ b/src/JsonApiDotNetCore/Repositories/ResourceRepositoryAccessor.cs @@ -29,6 +29,14 @@ public ResourceRepositoryAccessor(IServiceProvider serviceProvider, IResourceGra _request = request; } + /// + public ResourceType LookupResourceType(Type resourceClrType) + { + ArgumentGuard.NotNull(resourceClrType); + + return _resourceGraph.GetResourceType(resourceClrType); + } + /// public async Task> GetAsync(QueryLayer queryLayer, CancellationToken cancellationToken) where TResource : class, IIdentifiable diff --git a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs index bdaf89fbe0..4fa0cdc4f0 100644 --- a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs +++ b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs @@ -58,10 +58,10 @@ public virtual async Task> GetAsync(CancellationT { _traceWriter.LogMethodStart(); - using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Get resources"); - AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Get resources"); + if (_options.IncludeTotalResourceCount) { FilterExpression? topFilter = _queryLayerComposer.GetPrimaryFilterFromConstraints(_request.PrimaryResourceType); @@ -106,12 +106,12 @@ public virtual async Task GetAsync([DisallowNull] TId id, Cancellatio relationshipName }); - using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Get secondary resource(s)"); - ArgumentGuard.NotNull(relationshipName); AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType); AssertHasRelationship(_request.Relationship, relationshipName); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Get secondary resource(s)"); + if (_options.IncludeTotalResourceCount && _request.IsCollection) { await RetrieveResourceCountForNonPrimaryEndpointAsync(id, (HasManyAttribute)_request.Relationship, cancellationToken); @@ -146,12 +146,12 @@ public virtual async Task GetAsync([DisallowNull] TId id, Cancellatio relationshipName }); - using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Get relationship"); - ArgumentGuard.NotNull(relationshipName); AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType); AssertHasRelationship(_request.Relationship, relationshipName); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Get relationship"); + if (_options.IncludeTotalResourceCount && _request.IsCollection) { await RetrieveResourceCountForNonPrimaryEndpointAsync(id, (HasManyAttribute)_request.Relationship, cancellationToken); @@ -349,12 +349,12 @@ public virtual async Task AddToToManyRelationshipAsync([DisallowNull] TId leftId rightResourceIds }); - using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Add to to-many relationship"); - ArgumentGuard.NotNull(relationshipName); ArgumentGuard.NotNull(rightResourceIds); AssertHasRelationship(_request.Relationship, relationshipName); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Add to to-many relationship"); + TResource? resourceFromDatabase = null; if (rightResourceIds.Count > 0 && _request.Relationship is HasManyAttribute { IsManyToMany: true } manyToManyRelationship) @@ -499,11 +499,11 @@ public virtual async Task SetRelationshipAsync([DisallowNull] TId leftId, string rightValue }); - using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Set relationship"); - ArgumentGuard.NotNull(relationshipName); AssertHasRelationship(_request.Relationship, relationshipName); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Set relationship"); + object? effectiveRightValue = _request.Relationship.RightType.IsPartOfTypeHierarchy() // Some of the incoming right-side resources may be stored as a derived type. We fetch them, so we'll know // the stored types, which enables to invoke resource definitions with the stored right-side resources types. @@ -534,10 +534,10 @@ public virtual async Task DeleteAsync([DisallowNull] TId id, CancellationToken c id }); - using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Delete resource"); - AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType); + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Delete resource"); + TResource? resourceFromDatabase = null; if (_request.PrimaryResourceType.IsPartOfTypeHierarchy()) @@ -570,11 +570,12 @@ public virtual async Task RemoveFromToManyRelationshipAsync([DisallowNull] TId l rightResourceIds }); - using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Remove from to-many relationship"); - ArgumentGuard.NotNull(relationshipName); ArgumentGuard.NotNull(rightResourceIds); AssertHasRelationship(_request.Relationship, relationshipName); + + using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Remove from to-many relationship"); + var hasManyRelationship = (HasManyAttribute)_request.Relationship; TResource resourceFromDatabase = await GetForHasManyUpdateAsync(hasManyRelationship, leftId, rightResourceIds, cancellationToken); @@ -599,9 +600,10 @@ protected async Task GetPrimaryResourceByIdAsync([DisallowNull] TId i private async Task GetPrimaryResourceByIdOrDefaultAsync([DisallowNull] TId id, TopFieldSelection fieldSelection, CancellationToken cancellationToken) { - AssertPrimaryResourceTypeInJsonApiRequestIsNotNull(_request.PrimaryResourceType); + // Using the non-accurized resource type, so that includes on sibling derived types can be used at abstract endpoint. + ResourceType resourceType = _repositoryAccessor.LookupResourceType(typeof(TResource)); - QueryLayer primaryLayer = _queryLayerComposer.ComposeForGetById(id, _request.PrimaryResourceType, fieldSelection); + QueryLayer primaryLayer = _queryLayerComposer.ComposeForGetById(id, resourceType, fieldSelection); IReadOnlyCollection primaryResources = await _repositoryAccessor.GetAsync(primaryLayer, cancellationToken); return primaryResources.SingleOrDefault(); diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/ResourceInheritanceReadTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/ResourceInheritanceReadTests.cs index ec5c1c46f1..ec541d443d 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/ResourceInheritanceReadTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/ResourceInheritanceReadTests.cs @@ -20,6 +20,8 @@ protected ResourceInheritanceReadTests(IntegrationTestContext(); + testContext.UseController(); testContext.UseController(); testContext.UseController(); @@ -380,6 +382,53 @@ await _testContext.RunOnDatabaseAsync(async dbContext => responseDocument.Data.SingleValue.Relationships.ShouldOnlyContainKeys("manufacturer", "wheels", "cargoBox", "lights", "foldingDimensions", "features"); } + [Fact] + public async Task Can_get_primary_resource_with_derived_includes() + { + // Arrange + VehicleManufacturer manufacturer = _fakers.VehicleManufacturer.GenerateOne(); + + Bike bike = _fakers.Bike.GenerateOne(); + bike.Lights = _fakers.BicycleLight.GenerateSet(15); + manufacturer.Vehicles.Add(bike); + + Tandem tandem = _fakers.Tandem.GenerateOne(); + tandem.Features = _fakers.GenericFeature.GenerateSet(15); + manufacturer.Vehicles.Add(tandem); + + Car car = _fakers.Car.GenerateOne(); + car.Engine = _fakers.GasolineEngine.GenerateOne(); + car.Features = _fakers.GenericFeature.GenerateSet(15); + manufacturer.Vehicles.Add(car); + + Truck truck = _fakers.Truck.GenerateOne(); + truck.Engine = _fakers.GasolineEngine.GenerateOne(); + truck.Features = _fakers.GenericFeature.GenerateSet(15); + manufacturer.Vehicles.Add(truck); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.VehicleManufacturers.Add(manufacturer); + await dbContext.SaveChangesAsync(); + }); + + string route = $"/vehicleManufacturers/{manufacturer.StringId}?include=vehicles.lights,vehicles.features"; + + // Act + (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route); + + // Assert + httpResponse.ShouldHaveStatusCode(HttpStatusCode.OK); + + responseDocument.Data.SingleValue.ShouldNotBeNull(); + responseDocument.Data.SingleValue.Type.Should().Be("vehicleManufacturers"); + responseDocument.Data.SingleValue.Id.Should().Be(manufacturer.StringId); + + responseDocument.Included.ShouldNotBeNull(); + responseDocument.Included.Where(include => include.Type == "bicycleLights").Should().HaveCount(10); + responseDocument.Included.Where(include => include.Type == "genericFeatures").Should().HaveCount(10 * 3); + } + [Fact] public async Task Can_get_secondary_resource_at_abstract_base_endpoint() { @@ -1716,6 +1765,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => "id": "{{bike.Manufacturer.StringId}}", "attributes": { "name": "{{bike.Manufacturer.Name}}" + }, + "relationships": { + "vehicles": { + "links": { + "self": "/vehicleManufacturers/{{bike.Manufacturer.StringId}}/relationships/vehicles", + "related": "/vehicleManufacturers/{{bike.Manufacturer.StringId}}/vehicles" + } + } + }, + "links": { + "self": "/vehicleManufacturers/{{bike.Manufacturer.StringId}}" } }, { @@ -1822,6 +1882,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => "id": "{{car.Manufacturer.StringId}}", "attributes": { "name": "{{car.Manufacturer.Name}}" + }, + "relationships": { + "vehicles": { + "links": { + "self": "/vehicleManufacturers/{{car.Manufacturer.StringId}}/relationships/vehicles", + "related": "/vehicleManufacturers/{{car.Manufacturer.StringId}}/vehicles" + } + } + }, + "links": { + "self": "/vehicleManufacturers/{{car.Manufacturer.StringId}}" } }, { @@ -1903,6 +1974,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => "id": "{{tandem.Manufacturer.StringId}}", "attributes": { "name": "{{tandem.Manufacturer.Name}}" + }, + "relationships": { + "vehicles": { + "links": { + "self": "/vehicleManufacturers/{{tandem.Manufacturer.StringId}}/relationships/vehicles", + "related": "/vehicleManufacturers/{{tandem.Manufacturer.StringId}}/vehicles" + } + } + }, + "links": { + "self": "/vehicleManufacturers/{{tandem.Manufacturer.StringId}}" } }, { @@ -1997,6 +2079,17 @@ await _testContext.RunOnDatabaseAsync(async dbContext => "id": "{{truck.Manufacturer.StringId}}", "attributes": { "name": "{{truck.Manufacturer.Name}}" + }, + "relationships": { + "vehicles": { + "links": { + "self": "/vehicleManufacturers/{{truck.Manufacturer.StringId}}/relationships/vehicles", + "related": "/vehicleManufacturers/{{truck.Manufacturer.StringId}}/vehicles" + } + } + }, + "links": { + "self": "/vehicleManufacturers/{{truck.Manufacturer.StringId}}" } }, { diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/ResourceInheritanceWriteTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/ResourceInheritanceWriteTests.cs index a5cffa039a..c0620b12e0 100644 --- a/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/ResourceInheritanceWriteTests.cs +++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceInheritance/ResourceInheritanceWriteTests.cs @@ -108,7 +108,7 @@ public async Task Cannot_create_abstract_resource_at_abstract_endpoint() } [Fact] - public async Task Can_create_concrete_base_resource_at_abstract_endpoint_with_relationships() + public async Task Can_create_concrete_base_resource_at_abstract_endpoint_with_relationships_and_includes() { // Arrange var bikeStore = _testContext.Factory.Services.GetRequiredService>(); @@ -184,7 +184,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - const string route = "/vehicles"; + const string route = "/vehicles?include=manufacturer,wheels,engine,navigationSystem,features,sleepingArea,cargoBox,lights,foldingDimensions"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -238,7 +238,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Can_create_concrete_derived_resource_at_abstract_endpoint_with_relationships() + public async Task Can_create_concrete_derived_resource_at_abstract_endpoint_with_relationships_and_includes() { // Arrange var carStore = _testContext.Factory.Services.GetRequiredService>(); @@ -325,7 +325,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - const string route = "/vehicles"; + const string route = "/vehicles?include=manufacturer,wheels,engine,navigationSystem,features,sleepingArea,cargoBox,lights,foldingDimensions"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -385,7 +385,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Can_create_concrete_derived_resource_at_concrete_base_endpoint_with_relationships() + public async Task Can_create_concrete_derived_resource_at_concrete_base_endpoint_with_relationships_and_includes() { // Arrange var tandemStore = _testContext.Factory.Services.GetRequiredService>(); @@ -475,7 +475,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - const string route = "/bikes"; + const string route = "/bikes?include=manufacturer,wheels,cargoBox,lights,foldingDimensions,features"; // Act (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); @@ -980,7 +980,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Can_update_concrete_base_resource_at_abstract_endpoint_with_relationships() + public async Task Can_update_concrete_base_resource_at_abstract_endpoint_with_relationships_and_includes() { // Arrange var bikeStore = _testContext.Factory.Services.GetRequiredService>(); @@ -1059,7 +1059,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/vehicles/{existingBike.StringId}"; + string route = + $"/vehicles/{existingBike.StringId}?include=manufacturer,wheels,engine,navigationSystem,features,sleepingArea,cargoBox,lights,foldingDimensions"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); @@ -1108,7 +1109,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } [Fact] - public async Task Can_update_concrete_base_resource_stored_as_concrete_derived_at_abstract_endpoint_with_relationships() + public async Task Can_update_concrete_base_resource_stored_as_concrete_derived_at_abstract_endpoint_with_relationships_and_includes() { // Arrange var tandemStore = _testContext.Factory.Services.GetRequiredService>(); @@ -1187,7 +1188,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext => } }; - string route = $"/vehicles/{existingTandem.StringId}"; + string route = + $"/vehicles/{existingTandem.StringId}?include=manufacturer,wheels,engine,navigationSystem,features,sleepingArea,cargoBox,lights,foldingDimensions"; // Act (HttpResponseMessage httpResponse, string responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); diff --git a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/IncludeParseTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/IncludeParseTests.cs index 096f469952..fc3431da31 100644 --- a/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/IncludeParseTests.cs +++ b/test/JsonApiDotNetCoreTests/UnitTests/QueryStringParameters/IncludeParseTests.cs @@ -93,9 +93,9 @@ public void Reader_Read_Fails(string parameterName, string parameterValue, strin [InlineData("includes", "posts.comments", "posts.comments")] [InlineData("includes", "posts,posts.comments", "posts.comments")] [InlineData("includes", "posts,posts.labels,posts.comments", "posts.comments,posts.labels")] - [InlineData("includes", "owner.person.children.husband", "owner.person.children.husband")] + [InlineData("includes", "owner.person.children.husband", "owner.person.children.husband,owner.person.children.husband")] [InlineData("includes", "owner.person.wife,owner.person.husband", "owner.person.husband,owner.person.wife")] - [InlineData("includes", "owner.person.father.children.wife", "owner.person.father.children.wife")] + [InlineData("includes", "owner.person.father.children.wife", "owner.person.father.children.wife,owner.person.father.children.wife")] [InlineData("includes", "owner.person.friends", "owner.person.friends,owner.person.friends")] [InlineData("includes", "owner.person.friends.friends", "owner.person.friends.friends,owner.person.friends.friends,owner.person.friends.friends,owner.person.friends.friends")] From cfe159ba7c45f00953c239685230b6c1b6f27b8b Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:33:19 +0100 Subject: [PATCH 3/3] Fix broken build on .NET 9 SDK - Turn off scanning for vulnerable dependencies in downstream packages - Turn off warning when targeting NETSTANDARD 1.x --- Directory.Build.props | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Directory.Build.props b/Directory.Build.props index 6c68a15073..dc48433409 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,6 +10,8 @@ $(MSBuildThisFileDirectory)CodingGuidelines.ruleset $(MSBuildThisFileDirectory)tests.runsettings 5.6.1 + direct + $(NoWarn);NETSDK1215 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy