Skip to content

Commit

Permalink
Merge pull request #1645 from json-api-dotnet/merge-master-into-openapi
Browse files Browse the repository at this point in the history
Merge master into openapi
  • Loading branch information
bkoelman authored Nov 27, 2024
2 parents e5a7084 + 575f579 commit 9010f16
Show file tree
Hide file tree
Showing 29 changed files with 334 additions and 129 deletions.
2 changes: 2 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodingGuidelines.ruleset</CodeAnalysisRuleSet>
<RunSettingsFilePath>$(MSBuildThisFileDirectory)tests.runsettings</RunSettingsFilePath>
<JsonApiDotNetCoreVersionPrefix>5.6.1</JsonApiDotNetCoreVersionPrefix>
<NuGetAuditMode>direct</NuGetAuditMode>
<NoWarn>$(NoWarn);NETSDK1215</NoWarn>
</PropertyGroup>

<PropertyGroup>
Expand Down
1 change: 1 addition & 0 deletions JsonApiDotNetCore.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,7 @@ $left$ = $right$;</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=D29C3A091CD9E74BBFDF1B8974F5A977/SearchPattern/@EntryValue">if ($argument$ is null) throw new ArgumentNullException(nameof($argument$));</s:String>
<s:String x:Key="/Default/PatternsAndTemplates/StructuralSearch/Pattern/=D29C3A091CD9E74BBFDF1B8974F5A977/Severity/@EntryValue">WARNING</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Accurize/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=accurized/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=appsettings/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Assignee/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Contoso/@EntryIndexedValue">True</s:Boolean>
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 6 additions & 5 deletions src/Examples/DapperExample/Repositories/DapperRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ public sealed partial class DapperRepository<TResource, TId> : IResourceReposito
private readonly SqlCaptureStore _captureStore;
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger<DapperRepository<TResource, TId>> _logger;
private readonly CollectionConverter _collectionConverter = new();
private readonly ParameterFormatter _parameterFormatter = new();
private readonly DapperFacade _dapperFacade;

Expand Down Expand Up @@ -270,12 +269,12 @@ private async Task ApplyTargetedFieldsAsync(TResource resourceFromRequest, TReso

if (relationship is HasManyAttribute hasManyRelationship)
{
HashSet<IIdentifiable> rightResourceIds = _collectionConverter.ExtractResources(rightValue).ToHashSet(IdentifiableComparer.Instance);
HashSet<IIdentifiable> 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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ namespace DapperExample.Repositories;
/// </summary>
internal sealed class ResourceChangeDetector
{
private readonly CollectionConverter _collectionConverter = new();
private readonly IDataModelService _dataModelService;

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

relationshipValues[relationship] = rightResources;
}
Expand Down
11 changes: 5 additions & 6 deletions src/Examples/OpenApiKiotaClientExample/PeopleMessageFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,27 @@ private static string WritePeople(PersonCollectionResponseDocument? peopleRespon
return builder.ToString();
}

private static void WritePerson(PersonDataInResponse person, ICollection<DataInResponse> includes, StringBuilder builder)
private static void WritePerson(PersonDataInResponse person, List<DataInResponse> includes, StringBuilder builder)
{
ICollection<TodoItemIdentifierInResponse> assignedTodoItems = person.Relationships?.AssignedTodoItems?.Data ?? [];
List<TodoItemIdentifierInResponse> 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<TodoItemIdentifierInResponse> todoItemIdentifiers, ICollection<DataInResponse> includes,
StringBuilder builder)
private static void WriteRelatedTodoItems(List<TodoItemIdentifierInResponse> todoItemIdentifiers, List<DataInResponse> includes, StringBuilder builder)
{
foreach (TodoItemIdentifierInResponse todoItemIdentifier in todoItemIdentifiers)
{
TodoItemDataInResponse includedTodoItem = includes.OfType<TodoItemDataInResponse>().Single(include => include.Id == todoItemIdentifier.Id);
ICollection<TagIdentifierInResponse> tags = includedTodoItem.Relationships?.Tags?.Data ?? [];
List<TagIdentifierInResponse> 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<TagIdentifierInResponse> tagIdentifiers, ICollection<DataInResponse> includes, StringBuilder builder)
private static void WriteRelatedTags(List<TagIdentifierInResponse> tagIdentifiers, List<DataInResponse> includes, StringBuilder builder)
{
foreach (TagIdentifierInResponse tagIdentifier in tagIdentifiers)
{
Expand Down
34 changes: 29 additions & 5 deletions src/JsonApiDotNetCore.Annotations/CollectionConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,24 @@ internal sealed class CollectionConverter
typeof(IEnumerable<>)
];

public static CollectionConverter Instance { get; } = new();

private CollectionConverter()
{
}

/// <summary>
/// Creates a collection instance based on the specified collection type and copies the specified elements into it.
/// </summary>
/// <param name="source">
/// Source to copy from.
/// </param>
/// <param name="collectionType">
/// Target collection type, for example: typeof(List{Article}) or typeof(ISet{Person}).
/// Target collection type, for example: <code><![CDATA[
/// typeof(List<Article>)
/// ]]></code> or <code><![CDATA[
/// typeof(ISet<Person>)
/// ]]></code>.
/// </param>
public IEnumerable CopyToTypedCollection(IEnumerable source, Type collectionType)
{
Expand All @@ -41,7 +51,12 @@ public IEnumerable CopyToTypedCollection(IEnumerable source, Type collectionType
}

/// <summary>
/// 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: <code><![CDATA[
/// IList<Article> -> List<Article>
/// ]]></code> or
/// <code><![CDATA[
/// ISet<Article> -> HashSet<Article>
/// ]]></code>.
/// </summary>
private Type ToConcreteCollectionType(Type collectionType)
{
Expand Down Expand Up @@ -80,7 +95,12 @@ public IReadOnlyCollection<IIdentifiable> ExtractResources(object? value)
}

/// <summary>
/// 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: <code><![CDATA[
/// IList<string> -> string
/// ]]></code> or
/// <code><![CDATA[
/// IList -> null
/// ]]></code>.
/// </summary>
public Type? FindCollectionElementType(Type? type)
{
Expand All @@ -96,8 +116,12 @@ public IReadOnlyCollection<IIdentifiable> ExtractResources(object? value)
}

/// <summary>
/// Indicates whether a <see cref="HashSet{T}" /> instance can be assigned to the specified type, for example IList{Article} -> false or ISet{Article} ->
/// true.
/// Indicates whether a <see cref="HashSet{T}" /> instance can be assigned to the specified type, for example:
/// <code><![CDATA[
/// IList<Article> -> false
/// ]]></code> or <code><![CDATA[
/// ISet<Article> -> true
/// ]]></code>.
/// </summary>
public bool TypeCanContainHashSet(Type collectionType)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -103,14 +103,14 @@ public void AddValue(object resource, IIdentifiable resourceToAdd)
ArgumentGuard.NotNull(resourceToAdd);

object? rightValue = GetValue(resource);
List<IIdentifiable> rightResources = CollectionConverter.ExtractResources(rightValue).ToList();
List<IIdentifiable> 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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ namespace JsonApiDotNetCore.AtomicOperations.Processors;
public class SetRelationshipProcessor<TResource, TId> : ISetRelationshipProcessor<TResource, TId>
where TResource : class, IIdentifiable<TId>
{
private readonly CollectionConverter _collectionConverter = new();
private readonly ISetRelationshipService<TResource, TId> _service;

public SetRelationshipProcessor(ISetRelationshipService<TResource, TId> service)
Expand Down Expand Up @@ -40,7 +39,7 @@ public SetRelationshipProcessor(ISetRelationshipService<TResource, TId> service)

if (relationship is HasManyAttribute)
{
IReadOnlyCollection<IIdentifiable> rightResources = _collectionConverter.ExtractResources(rightValue);
IReadOnlyCollection<IIdentifiable> rightResources = CollectionConverter.Instance.ExtractResources(rightValue);
return rightResources.ToHashSet(IdentifiableComparer.Instance);
}

Expand Down
4 changes: 1 addition & 3 deletions src/JsonApiDotNetCore/Errors/InvalidModelStateException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -319,8 +319,6 @@ private sealed class ArrayIndexerSegment(
Func<Type, int, Type?>? getCollectionElementTypeCallback)
: ModelStateKeySegment(modelType, isInComplexType, nextKey, sourcePointer, parent, getCollectionElementTypeCallback)
{
private static readonly CollectionConverter CollectionConverter = new();

public int ArrayIndex { get; } = arrayIndex;

public Type GetCollectionElementType()
Expand All @@ -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)
{
Expand Down
23 changes: 21 additions & 2 deletions src/JsonApiDotNetCore/Middleware/JsonApiContentNegotiator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net;
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Serialization.Objects;
Expand All @@ -8,6 +9,7 @@
namespace JsonApiDotNetCore.Middleware;

/// <inheritdoc />
[PublicAPI]
public class JsonApiContentNegotiator : IJsonApiContentNegotiator
{
private readonly IJsonApiOptions _options;
Expand Down Expand Up @@ -71,9 +73,9 @@ private IReadOnlySet<JsonApiMediaTypeExtension> 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
{
Expand Down Expand Up @@ -149,6 +151,23 @@ private IReadOnlySet<JsonApiMediaTypeExtension> ValidateAcceptHeader(IReadOnlyLi
return bestMatch.Extensions;
}

/// <summary>
/// Returns the JSON:API media type (possibly including extensions) to use when no Accept header was sent.
/// </summary>
/// <param name="possibleMediaTypes">
/// The media types returned from <see cref="GetPossibleMediaTypes" />.
/// </param>
/// <param name="requestMediaType">
/// The media type from in the Content-Type header.
/// </param>
/// <returns>
/// The default media type to use, or <c>null</c> if not available.
/// </returns>
protected virtual JsonApiMediaType? GetDefaultMediaType(IReadOnlyList<JsonApiMediaType> possibleMediaTypes, JsonApiMediaType? requestMediaType)
{
return possibleMediaTypes.Contains(JsonApiMediaType.Default) ? JsonApiMediaType.Default : null;
}

/// <summary>
/// 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.
Expand Down
28 changes: 27 additions & 1 deletion src/JsonApiDotNetCore/Queries/Parsing/IncludeParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ private static ReadOnlyCollection<IncludeTreeNode> 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<RelationshipAttribute> relationships = parent.Relationship.RightType.GetRelationshipsInTypeOrDerived(relationshipName);
HashSet<RelationshipAttribute> relationships = GetRelationshipsInConcreteTypes(parent.Relationship.RightType, relationshipName);

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

private static HashSet<RelationshipAttribute> GetRelationshipsInConcreteTypes(ResourceType resourceType, string relationshipName)
{
HashSet<RelationshipAttribute> 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<RelationshipAttribute> relationshipsToInclude)
{
foreach (ResourceType derivedType in relationship.LeftType.GetAllConcreteDerivedTypes())
{
RelationshipAttribute relationshipInDerived = derivedType.GetRelationshipByPublicName(relationship.PublicName);
relationshipsToInclude.Add(relationshipInDerived);
}
}

private static void AssertRelationshipsFound(HashSet<RelationshipAttribute> relationshipsFound, string relationshipName,
IReadOnlyCollection<IncludeTreeNode> parents, int position)
{
Expand Down
Loading

0 comments on commit 9010f16

Please sign in to comment.