diff --git a/Directory.Build.props b/Directory.Build.props
index 6c68a1507..dc4843340 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
diff --git a/JsonApiDotNetCore.sln.DotSettings b/JsonApiDotNetCore.sln.DotSettings
index 31abaa8dc..7cbbf8235 100644
--- a/JsonApiDotNetCore.sln.DotSettings
+++ b/JsonApiDotNetCore.sln.DotSettings
@@ -672,6 +672,7 @@ $left$ = $right$;
if ($argument$ is null) throw new ArgumentNullException(nameof($argument$));
WARNING
True
+ True
True
True
True
diff --git a/README.md b/README.md
index 5ae9184e0..3354eac45 100644
--- a/README.md
+++ b/README.md
@@ -85,13 +85,16 @@ See also our [versioning policy](./VERSIONING_POLICY.md).
| | | 7 | 7 |
| 5.5+ | Stable | 6 | 6, 7 |
| | | 7 | 7 |
-| | | 8 | 8 |
+| | | 8 | 8, 9 |
+| | | 9 | 9 |
| master | Preview | 6 | 6, 7 |
| | | 7 | 7 |
-| | | 8 | 8 |
+| | | 8 | 8, 9 |
+| | | 9 | 9 |
| openapi | Experimental | 6 | 6, 7 |
| | | 7 | 7 |
-| | | 8 | 8 |
+| | | 8 | 8, 9 |
+| | | 9 | 9 |
## Contributing
diff --git a/src/Examples/DapperExample/Repositories/DapperRepository.cs b/src/Examples/DapperExample/Repositories/DapperRepository.cs
index 1da38697d..7a46e69a5 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 6b075d6ca..73213445e 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/Examples/OpenApiKiotaClientExample/PeopleMessageFormatter.cs b/src/Examples/OpenApiKiotaClientExample/PeopleMessageFormatter.cs
index 69898f107..83b3d6264 100644
--- a/src/Examples/OpenApiKiotaClientExample/PeopleMessageFormatter.cs
+++ b/src/Examples/OpenApiKiotaClientExample/PeopleMessageFormatter.cs
@@ -34,28 +34,27 @@ private static string WritePeople(PersonCollectionResponseDocument? peopleRespon
return builder.ToString();
}
- private static void WritePerson(PersonDataInResponse person, ICollection includes, StringBuilder builder)
+ private static void WritePerson(PersonDataInResponse person, List includes, StringBuilder builder)
{
- ICollection assignedTodoItems = person.Relationships?.AssignedTodoItems?.Data ?? [];
+ List assignedTodoItems = person.Relationships?.AssignedTodoItems?.Data ?? [];
builder.AppendLine($" Person {person.Id}: {person.Attributes?.DisplayName} with {assignedTodoItems.Count} assigned todo-items:");
WriteRelatedTodoItems(assignedTodoItems, includes, builder);
}
- private static void WriteRelatedTodoItems(IEnumerable todoItemIdentifiers, ICollection includes,
- StringBuilder builder)
+ private static void WriteRelatedTodoItems(List todoItemIdentifiers, List includes, StringBuilder builder)
{
foreach (TodoItemIdentifierInResponse todoItemIdentifier in todoItemIdentifiers)
{
TodoItemDataInResponse includedTodoItem = includes.OfType().Single(include => include.Id == todoItemIdentifier.Id);
- ICollection tags = includedTodoItem.Relationships?.Tags?.Data ?? [];
+ List tags = includedTodoItem.Relationships?.Tags?.Data ?? [];
builder.AppendLine($" TodoItem {includedTodoItem.Id}: {includedTodoItem.Attributes?.Description} with {tags.Count} tags:");
WriteRelatedTags(tags, includes, builder);
}
}
- private static void WriteRelatedTags(IEnumerable tagIdentifiers, ICollection includes, StringBuilder builder)
+ private static void WriteRelatedTags(List tagIdentifiers, List includes, StringBuilder builder)
{
foreach (TagIdentifierInResponse tagIdentifier in tagIdentifiers)
{
diff --git a/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs b/src/JsonApiDotNetCore.Annotations/CollectionConverter.cs
index 750b896c2..f055c6548 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 43161f99a..274a6bb16 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 72212c76f..51d22f995 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 492af08c6..72bdecf7b 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 3eeaa77fb..abc40c481 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 d55243e30..ccf23e1eb 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/Middleware/JsonApiContentNegotiator.cs b/src/JsonApiDotNetCore/Middleware/JsonApiContentNegotiator.cs
index 5a3c08ee2..9cc366b28 100644
--- a/src/JsonApiDotNetCore/Middleware/JsonApiContentNegotiator.cs
+++ b/src/JsonApiDotNetCore/Middleware/JsonApiContentNegotiator.cs
@@ -1,4 +1,5 @@
using System.Net;
+using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Serialization.Objects;
@@ -8,6 +9,7 @@
namespace JsonApiDotNetCore.Middleware;
///
+[PublicAPI]
public class JsonApiContentNegotiator : IJsonApiContentNegotiator
{
private readonly IJsonApiOptions _options;
@@ -71,9 +73,9 @@ private IReadOnlySet ValidateAcceptHeader(IReadOnlyLi
string[] acceptHeaderValues = HttpContext.Request.Headers.GetCommaSeparatedValues("Accept");
JsonApiMediaType? bestMatch = null;
- if (acceptHeaderValues.Length == 0 && possibleMediaTypes.Contains(JsonApiMediaType.Default))
+ if (acceptHeaderValues.Length == 0)
{
- bestMatch = JsonApiMediaType.Default;
+ bestMatch = GetDefaultMediaType(possibleMediaTypes, requestMediaType);
}
else
{
@@ -149,6 +151,23 @@ private IReadOnlySet ValidateAcceptHeader(IReadOnlyLi
return bestMatch.Extensions;
}
+ ///
+ /// Returns the JSON:API media type (possibly including extensions) to use when no Accept header was sent.
+ ///
+ ///
+ /// The media types returned from .
+ ///
+ ///
+ /// The media type from in the Content-Type header.
+ ///
+ ///
+ /// The default media type to use, or null if not available.
+ ///
+ protected virtual JsonApiMediaType? GetDefaultMediaType(IReadOnlyList possibleMediaTypes, JsonApiMediaType? requestMediaType)
+ {
+ return possibleMediaTypes.Contains(JsonApiMediaType.Default) ? JsonApiMediaType.Default : null;
+ }
+
///
/// Gets the list of possible combinations of JSON:API extensions that are available at the current endpoint. The set of extensions in the request body
/// must always be the same as in the response body.
diff --git a/src/JsonApiDotNetCore/Queries/Parsing/IncludeParser.cs b/src/JsonApiDotNetCore/Queries/Parsing/IncludeParser.cs
index 99674b239..6afa8a069 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 80125cde5..1804027de 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;
@@ -213,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))
{
@@ -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/QueryClauseBuilderContext.cs b/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryClauseBuilderContext.cs
index 05cccf794..b218b09ca 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 6b52b3d69..c04eb6882 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 7e099fb09..9d2498620 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;
@@ -30,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)
{
@@ -74,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);
}
}
@@ -101,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);
}
@@ -183,35 +191,40 @@ private static void IncludeEagerLoads(ResourceType resourceType, Dictionary : 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/Repositories/IResourceRepositoryAccessor.cs b/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs
index a58542c23..df08f04f2 100644
--- a/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs
+++ b/src/JsonApiDotNetCore/Repositories/IResourceRepositoryAccessor.cs
@@ -11,6 +11,11 @@ namespace JsonApiDotNetCore.Repositories;
///
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 23d03eb9d..827c2259f 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/Resources/OperationContainer.cs b/src/JsonApiDotNetCore/Resources/OperationContainer.cs
index 88ea29ecd..2c8ec00d1 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 b0b230094..02b3e80e4 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 0e4e605d9..3556eadaf 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 2fc11684f..4fa0cdc4f 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;
@@ -59,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);
@@ -107,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);
@@ -147,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);
@@ -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);
}
@@ -350,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)
@@ -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)
{
@@ -500,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.
@@ -535,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())
@@ -571,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);
@@ -600,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 ec5c1c46f..ec541d443 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 a5cffa039..c0620b12e 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 096f46995..fc3431da3 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")]