Skip to content

Commit 9010f16

Browse files
authored
Merge pull request #1645 from json-api-dotnet/merge-master-into-openapi
Merge master into openapi
2 parents e5a7084 + 575f579 commit 9010f16

29 files changed

+334
-129
lines changed

Directory.Build.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodingGuidelines.ruleset</CodeAnalysisRuleSet>
1111
<RunSettingsFilePath>$(MSBuildThisFileDirectory)tests.runsettings</RunSettingsFilePath>
1212
<JsonApiDotNetCoreVersionPrefix>5.6.1</JsonApiDotNetCoreVersionPrefix>
13+
<NuGetAuditMode>direct</NuGetAuditMode>
14+
<NoWarn>$(NoWarn);NETSDK1215</NoWarn>
1315
</PropertyGroup>
1416

1517
<PropertyGroup>

JsonApiDotNetCore.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,7 @@ $left$ = $right$;</s:String>
672672
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=D29C3A091CD9E74BBFDF1B8974F5A977/SearchPattern/@EntryValue">if ($argument$ is null) throw new ArgumentNullException(nameof($argument$));</s:String>
673673
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=D29C3A091CD9E74BBFDF1B8974F5A977/Severity/@EntryValue">WARNING</s:String>
674674
<s:Boolean x:Key="/Default/UserDictionary/Words/=Accurize/@EntryIndexedValue">True</s:Boolean>
675+
<s:Boolean x:Key="/Default/UserDictionary/Words/=accurized/@EntryIndexedValue">True</s:Boolean>
675676
<s:Boolean x:Key="/Default/UserDictionary/Words/=appsettings/@EntryIndexedValue">True</s:Boolean>
676677
<s:Boolean x:Key="/Default/UserDictionary/Words/=Assignee/@EntryIndexedValue">True</s:Boolean>
677678
<s:Boolean x:Key="/Default/UserDictionary/Words/=Contoso/@EntryIndexedValue">True</s:Boolean>

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,16 @@ See also our [versioning policy](./VERSIONING_POLICY.md).
8585
| | | 7 | 7 |
8686
| 5.5+ | Stable | 6 | 6, 7 |
8787
| | | 7 | 7 |
88-
| | | 8 | 8 |
88+
| | | 8 | 8, 9 |
89+
| | | 9 | 9 |
8990
| master | Preview | 6 | 6, 7 |
9091
| | | 7 | 7 |
91-
| | | 8 | 8 |
92+
| | | 8 | 8, 9 |
93+
| | | 9 | 9 |
9294
| openapi | Experimental | 6 | 6, 7 |
9395
| | | 7 | 7 |
94-
| | | 8 | 8 |
96+
| | | 8 | 8, 9 |
97+
| | | 9 | 9 |
9598

9699
## Contributing
97100

src/Examples/DapperExample/Repositories/DapperRepository.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ public sealed partial class DapperRepository<TResource, TId> : IResourceReposito
103103
private readonly SqlCaptureStore _captureStore;
104104
private readonly ILoggerFactory _loggerFactory;
105105
private readonly ILogger<DapperRepository<TResource, TId>> _logger;
106-
private readonly CollectionConverter _collectionConverter = new();
107106
private readonly ParameterFormatter _parameterFormatter = new();
108107
private readonly DapperFacade _dapperFacade;
109108

@@ -270,12 +269,12 @@ private async Task ApplyTargetedFieldsAsync(TResource resourceFromRequest, TReso
270269

271270
if (relationship is HasManyAttribute hasManyRelationship)
272271
{
273-
HashSet<IIdentifiable> rightResourceIds = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance);
272+
HashSet<IIdentifiable> rightResourceIds = CollectionConverter.Instance.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance);
274273

275274
await _resourceDefinitionAccessor.OnSetToManyRelationshipAsync(leftResource, hasManyRelationship, rightResourceIds, writeOperation,
276275
cancellationToken);
277276

278-
return _collectionConverter.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType);
277+
return CollectionConverter.Instance.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType);
279278
}
280279

281280
return rightValue;
@@ -464,7 +463,9 @@ public async Task AddToToManyRelationshipAsync(TResource? leftResource, [Disallo
464463
leftPlaceholderResource.Id = leftId;
465464

466465
await _resourceDefinitionAccessor.OnAddToRelationshipAsync(leftPlaceholderResource, relationship, rightResourceIds, cancellationToken);
467-
relationship.SetValue(leftPlaceholderResource, _collectionConverter.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType));
466+
467+
relationship.SetValue(leftPlaceholderResource,
468+
CollectionConverter.Instance.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType));
468469

469470
await _resourceDefinitionAccessor.OnWritingAsync(leftPlaceholderResource, WriteOperationKind.AddToRelationship, cancellationToken);
470471

@@ -500,7 +501,7 @@ public async Task RemoveFromToManyRelationshipAsync(TResource leftResource, ISet
500501
var relationship = (HasManyAttribute)_targetedFields.Relationships.Single();
501502

502503
await _resourceDefinitionAccessor.OnRemoveFromRelationshipAsync(leftResource, relationship, rightResourceIds, cancellationToken);
503-
relationship.SetValue(leftResource, _collectionConverter.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType));
504+
relationship.SetValue(leftResource, CollectionConverter.Instance.CopyToTypedCollection(rightResourceIds, relationship.Property.PropertyType));
504505

505506
await _resourceDefinitionAccessor.OnWritingAsync(leftResource, WriteOperationKind.RemoveFromRelationship, cancellationToken);
506507

src/Examples/DapperExample/Repositories/ResourceChangeDetector.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ namespace DapperExample.Repositories;
1212
/// </summary>
1313
internal sealed class ResourceChangeDetector
1414
{
15-
private readonly CollectionConverter _collectionConverter = new();
1615
private readonly IDataModelService _dataModelService;
1716

1817
private Dictionary<string, object?> _currentColumnValues = [];
@@ -69,7 +68,7 @@ private Dictionary<RelationshipAttribute, HashSet<IIdentifiable>> CaptureRightRe
6968
foreach (RelationshipAttribute relationship in ResourceType.Relationships)
7069
{
7170
object? rightValue = relationship.GetValue(resource);
72-
HashSet<IIdentifiable> rightResources = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance);
71+
HashSet<IIdentifiable> rightResources = CollectionConverter.Instance.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance);
7372

7473
relationshipValues[relationship] = rightResources;
7574
}

src/Examples/OpenApiKiotaClientExample/PeopleMessageFormatter.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,28 +34,27 @@ private static string WritePeople(PersonCollectionResponseDocument? peopleRespon
3434
return builder.ToString();
3535
}
3636

37-
private static void WritePerson(PersonDataInResponse person, ICollection<DataInResponse> includes, StringBuilder builder)
37+
private static void WritePerson(PersonDataInResponse person, List<DataInResponse> includes, StringBuilder builder)
3838
{
39-
ICollection<TodoItemIdentifierInResponse> assignedTodoItems = person.Relationships?.AssignedTodoItems?.Data ?? [];
39+
List<TodoItemIdentifierInResponse> assignedTodoItems = person.Relationships?.AssignedTodoItems?.Data ?? [];
4040

4141
builder.AppendLine($" Person {person.Id}: {person.Attributes?.DisplayName} with {assignedTodoItems.Count} assigned todo-items:");
4242
WriteRelatedTodoItems(assignedTodoItems, includes, builder);
4343
}
4444

45-
private static void WriteRelatedTodoItems(IEnumerable<TodoItemIdentifierInResponse> todoItemIdentifiers, ICollection<DataInResponse> includes,
46-
StringBuilder builder)
45+
private static void WriteRelatedTodoItems(List<TodoItemIdentifierInResponse> todoItemIdentifiers, List<DataInResponse> includes, StringBuilder builder)
4746
{
4847
foreach (TodoItemIdentifierInResponse todoItemIdentifier in todoItemIdentifiers)
4948
{
5049
TodoItemDataInResponse includedTodoItem = includes.OfType<TodoItemDataInResponse>().Single(include => include.Id == todoItemIdentifier.Id);
51-
ICollection<TagIdentifierInResponse> tags = includedTodoItem.Relationships?.Tags?.Data ?? [];
50+
List<TagIdentifierInResponse> tags = includedTodoItem.Relationships?.Tags?.Data ?? [];
5251

5352
builder.AppendLine($" TodoItem {includedTodoItem.Id}: {includedTodoItem.Attributes?.Description} with {tags.Count} tags:");
5453
WriteRelatedTags(tags, includes, builder);
5554
}
5655
}
5756

58-
private static void WriteRelatedTags(IEnumerable<TagIdentifierInResponse> tagIdentifiers, ICollection<DataInResponse> includes, StringBuilder builder)
57+
private static void WriteRelatedTags(List<TagIdentifierInResponse> tagIdentifiers, List<DataInResponse> includes, StringBuilder builder)
5958
{
6059
foreach (TagIdentifierInResponse tagIdentifier in tagIdentifiers)
6160
{

src/JsonApiDotNetCore.Annotations/CollectionConverter.cs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,24 @@ internal sealed class CollectionConverter
1515
typeof(IEnumerable<>)
1616
];
1717

18+
public static CollectionConverter Instance { get; } = new();
19+
20+
private CollectionConverter()
21+
{
22+
}
23+
1824
/// <summary>
1925
/// Creates a collection instance based on the specified collection type and copies the specified elements into it.
2026
/// </summary>
2127
/// <param name="source">
2228
/// Source to copy from.
2329
/// </param>
2430
/// <param name="collectionType">
25-
/// Target collection type, for example: typeof(List{Article}) or typeof(ISet{Person}).
31+
/// Target collection type, for example: <code><![CDATA[
32+
/// typeof(List<Article>)
33+
/// ]]></code> or <code><![CDATA[
34+
/// typeof(ISet<Person>)
35+
/// ]]></code>.
2636
/// </param>
2737
public IEnumerable CopyToTypedCollection(IEnumerable source, Type collectionType)
2838
{
@@ -41,7 +51,12 @@ public IEnumerable CopyToTypedCollection(IEnumerable source, Type collectionType
4151
}
4252

4353
/// <summary>
44-
/// Returns a compatible collection type that can be instantiated, for example IList{Article} -> List{Article} or ISet{Article} -> HashSet{Article}
54+
/// Returns a compatible collection type that can be instantiated, for example: <code><![CDATA[
55+
/// IList<Article> -> List<Article>
56+
/// ]]></code> or
57+
/// <code><![CDATA[
58+
/// ISet<Article> -> HashSet<Article>
59+
/// ]]></code>.
4560
/// </summary>
4661
private Type ToConcreteCollectionType(Type collectionType)
4762
{
@@ -80,7 +95,12 @@ public IReadOnlyCollection<IIdentifiable> ExtractResources(object? value)
8095
}
8196

8297
/// <summary>
83-
/// Returns the element type if the specified type is a generic collection, for example: IList{string} -> string or IList -> null.
98+
/// Returns the element type if the specified type is a generic collection, for example: <code><![CDATA[
99+
/// IList<string> -> string
100+
/// ]]></code> or
101+
/// <code><![CDATA[
102+
/// IList -> null
103+
/// ]]></code>.
84104
/// </summary>
85105
public Type? FindCollectionElementType(Type? type)
86106
{
@@ -96,8 +116,12 @@ public IReadOnlyCollection<IIdentifiable> ExtractResources(object? value)
96116
}
97117

98118
/// <summary>
99-
/// Indicates whether a <see cref="HashSet{T}" /> instance can be assigned to the specified type, for example IList{Article} -> false or ISet{Article} ->
100-
/// true.
119+
/// Indicates whether a <see cref="HashSet{T}" /> instance can be assigned to the specified type, for example:
120+
/// <code><![CDATA[
121+
/// IList<Article> -> false
122+
/// ]]></code> or <code><![CDATA[
123+
/// ISet<Article> -> true
124+
/// ]]></code>.
101125
/// </summary>
102126
public bool TypeCanContainHashSet(Type collectionType)
103127
{

src/JsonApiDotNetCore.Annotations/Resources/Annotations/HasManyAttribute.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ private bool EvaluateIsManyToMany()
5959
{
6060
if (InverseNavigationProperty != null)
6161
{
62-
Type? elementType = CollectionConverter.FindCollectionElementType(InverseNavigationProperty.PropertyType);
62+
Type? elementType = CollectionConverter.Instance.FindCollectionElementType(InverseNavigationProperty.PropertyType);
6363
return elementType != null;
6464
}
6565

@@ -103,14 +103,14 @@ public void AddValue(object resource, IIdentifiable resourceToAdd)
103103
ArgumentGuard.NotNull(resourceToAdd);
104104

105105
object? rightValue = GetValue(resource);
106-
List<IIdentifiable> rightResources = CollectionConverter.ExtractResources(rightValue).ToList();
106+
List<IIdentifiable> rightResources = CollectionConverter.Instance.ExtractResources(rightValue).ToList();
107107

108108
if (!rightResources.Exists(nextResource => nextResource == resourceToAdd))
109109
{
110110
rightResources.Add(resourceToAdd);
111111

112112
Type collectionType = rightValue?.GetType() ?? Property.PropertyType;
113-
IEnumerable typedCollection = CollectionConverter.CopyToTypedCollection(rightResources, collectionType);
113+
IEnumerable typedCollection = CollectionConverter.Instance.CopyToTypedCollection(rightResources, collectionType);
114114
base.SetValue(resource, typedCollection);
115115
}
116116
}

src/JsonApiDotNetCore.Annotations/Resources/Annotations/HasOneAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ private bool EvaluateIsOneToOne()
5757
{
5858
if (InverseNavigationProperty != null)
5959
{
60-
Type? elementType = CollectionConverter.FindCollectionElementType(InverseNavigationProperty.PropertyType);
60+
Type? elementType = CollectionConverter.Instance.FindCollectionElementType(InverseNavigationProperty.PropertyType);
6161
return elementType == null;
6262
}
6363

src/JsonApiDotNetCore.Annotations/Resources/Annotations/RelationshipAttribute.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ namespace JsonApiDotNetCore.Resources.Annotations;
1212
[PublicAPI]
1313
public abstract class RelationshipAttribute : ResourceFieldAttribute
1414
{
15-
private protected static readonly CollectionConverter CollectionConverter = new();
16-
1715
// This field is definitely assigned after building the resource graph, which is why its public equivalent is declared as non-nullable.
1816
private ResourceType? _rightType;
1917

src/JsonApiDotNetCore/AtomicOperations/Processors/SetRelationshipProcessor.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors;
1010
public class SetRelationshipProcessor<TResource, TId> : ISetRelationshipProcessor<TResource, TId>
1111
where TResource : class, IIdentifiable<TId>
1212
{
13-
private readonly CollectionConverter _collectionConverter = new();
1413
private readonly ISetRelationshipService<TResource, TId> _service;
1514

1615
public SetRelationshipProcessor(ISetRelationshipService<TResource, TId> service)
@@ -40,7 +39,7 @@ public SetRelationshipProcessor(ISetRelationshipService<TResource, TId> service)
4039

4140
if (relationship is HasManyAttribute)
4241
{
43-
IReadOnlyCollection<IIdentifiable> rightResources = _collectionConverter.ExtractResources(rightValue);
42+
IReadOnlyCollection<IIdentifiable> rightResources = CollectionConverter.Instance.ExtractResources(rightValue);
4443
return rightResources.ToHashSet(IdentifiableComparer.Instance);
4544
}
4645

src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,8 +319,6 @@ private sealed class ArrayIndexerSegment(
319319
Func<Type, int, Type?>? getCollectionElementTypeCallback)
320320
: ModelStateKeySegment(modelType, isInComplexType, nextKey, sourcePointer, parent, getCollectionElementTypeCallback)
321321
{
322-
private static readonly CollectionConverter CollectionConverter = new();
323-
324322
public int ArrayIndex { get; } = arrayIndex;
325323

326324
public Type GetCollectionElementType()
@@ -333,7 +331,7 @@ private Type GetDeclaredCollectionElementType()
333331
{
334332
if (ModelType != typeof(string))
335333
{
336-
Type? elementType = CollectionConverter.FindCollectionElementType(ModelType);
334+
Type? elementType = CollectionConverter.Instance.FindCollectionElementType(ModelType);
337335

338336
if (elementType != null)
339337
{

src/JsonApiDotNetCore/Middleware/JsonApiContentNegotiator.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Net;
2+
using JetBrains.Annotations;
23
using JsonApiDotNetCore.Configuration;
34
using JsonApiDotNetCore.Errors;
45
using JsonApiDotNetCore.Serialization.Objects;
@@ -8,6 +9,7 @@
89
namespace JsonApiDotNetCore.Middleware;
910

1011
/// <inheritdoc />
12+
[PublicAPI]
1113
public class JsonApiContentNegotiator : IJsonApiContentNegotiator
1214
{
1315
private readonly IJsonApiOptions _options;
@@ -71,9 +73,9 @@ private IReadOnlySet<JsonApiMediaTypeExtension> ValidateAcceptHeader(IReadOnlyLi
7173
string[] acceptHeaderValues = HttpContext.Request.Headers.GetCommaSeparatedValues("Accept");
7274
JsonApiMediaType? bestMatch = null;
7375

74-
if (acceptHeaderValues.Length == 0 && possibleMediaTypes.Contains(JsonApiMediaType.Default))
76+
if (acceptHeaderValues.Length == 0)
7577
{
76-
bestMatch = JsonApiMediaType.Default;
78+
bestMatch = GetDefaultMediaType(possibleMediaTypes, requestMediaType);
7779
}
7880
else
7981
{
@@ -149,6 +151,23 @@ private IReadOnlySet<JsonApiMediaTypeExtension> ValidateAcceptHeader(IReadOnlyLi
149151
return bestMatch.Extensions;
150152
}
151153

154+
/// <summary>
155+
/// Returns the JSON:API media type (possibly including extensions) to use when no Accept header was sent.
156+
/// </summary>
157+
/// <param name="possibleMediaTypes">
158+
/// The media types returned from <see cref="GetPossibleMediaTypes" />.
159+
/// </param>
160+
/// <param name="requestMediaType">
161+
/// The media type from in the Content-Type header.
162+
/// </param>
163+
/// <returns>
164+
/// The default media type to use, or <c>null</c> if not available.
165+
/// </returns>
166+
protected virtual JsonApiMediaType? GetDefaultMediaType(IReadOnlyList<JsonApiMediaType> possibleMediaTypes, JsonApiMediaType? requestMediaType)
167+
{
168+
return possibleMediaTypes.Contains(JsonApiMediaType.Default) ? JsonApiMediaType.Default : null;
169+
}
170+
152171
/// <summary>
153172
/// 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
154173
/// must always be the same as in the response body.

src/JsonApiDotNetCore/Queries/Parsing/IncludeParser.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ private static ReadOnlyCollection<IncludeTreeNode> LookupRelationshipName(string
122122
{
123123
// Depending on the left side of the include chain, we may match relationships anywhere in the resource type hierarchy.
124124
// This is compensated for when rendering the response, which substitutes relationships on base types with the derived ones.
125-
IReadOnlySet<RelationshipAttribute> relationships = parent.Relationship.RightType.GetRelationshipsInTypeOrDerived(relationshipName);
125+
HashSet<RelationshipAttribute> relationships = GetRelationshipsInConcreteTypes(parent.Relationship.RightType, relationshipName);
126126

127127
if (relationships.Count > 0)
128128
{
@@ -140,6 +140,32 @@ private static ReadOnlyCollection<IncludeTreeNode> LookupRelationshipName(string
140140
return children.AsReadOnly();
141141
}
142142

143+
private static HashSet<RelationshipAttribute> GetRelationshipsInConcreteTypes(ResourceType resourceType, string relationshipName)
144+
{
145+
HashSet<RelationshipAttribute> relationshipsToInclude = [];
146+
147+
foreach (RelationshipAttribute relationship in resourceType.GetRelationshipsInTypeOrDerived(relationshipName))
148+
{
149+
if (!relationship.LeftType.ClrType.IsAbstract)
150+
{
151+
relationshipsToInclude.Add(relationship);
152+
}
153+
154+
IncludeRelationshipsFromConcreteDerivedTypes(relationship, relationshipsToInclude);
155+
}
156+
157+
return relationshipsToInclude;
158+
}
159+
160+
private static void IncludeRelationshipsFromConcreteDerivedTypes(RelationshipAttribute relationship, HashSet<RelationshipAttribute> relationshipsToInclude)
161+
{
162+
foreach (ResourceType derivedType in relationship.LeftType.GetAllConcreteDerivedTypes())
163+
{
164+
RelationshipAttribute relationshipInDerived = derivedType.GetRelationshipByPublicName(relationship.PublicName);
165+
relationshipsToInclude.Add(relationshipInDerived);
166+
}
167+
}
168+
143169
private static void AssertRelationshipsFound(HashSet<RelationshipAttribute> relationshipsFound, string relationshipName,
144170
IReadOnlyCollection<IncludeTreeNode> parents, int position)
145171
{

0 commit comments

Comments
 (0)