Skip to content

Merge master into openapi #1714

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 20, 2025
2 changes: 1 addition & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Bugs are tracked as [GitHub issues](https://github.com/json-api-dotnet/JsonApiDo
Explain the problem and include additional details to help maintainers reproduce the problem:

- **Use a clear and descriptive title** for the issue to identify the problem.
- **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, don't just say what you did, but explain how you did it.
- **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, don't just say what you did, but explain how you did it.
- **Provide specific examples to demonstrate the steps.** Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://docs.github.com/en/github/writing-on-github/creating-and-highlighting-code-blocks).
- **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. Explain which behavior you expected to see instead and why.
- **If you're reporting a crash**, include the full exception stack trace.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ To try it out, follow the steps below:
In the command above:
- Replace YOUR-GITHUB-USERNAME with the username you use to login your GitHub account.
- Replace YOUR-PAT-CLASSIC with the token your created above.

:warning: If the above command doesn't give you access in the next step, remove the package source by running:
```bash
dotnet nuget remove source github-json-api
Expand Down
2 changes: 1 addition & 1 deletion docs/ext/openapi/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Here's how an article (i.e. a resource of type "articles") might appear in a doc

### Atomic Operations

In addition to the members allowed by the [Atomic Operations extension](https://jsonapi.org/ext/atomic/),
In addition to the members allowed by the [Atomic Operations extension](https://jsonapi.org/ext/atomic/),
the following member MAY be included in elements of an `atomic:operations` array:

* `openapi:discriminator` - A free-format string to facilitate generation of client code.
Expand Down
4 changes: 2 additions & 2 deletions docs/usage/extensibility/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
The default middleware validates incoming `Content-Type` and `Accept` HTTP headers.
Based on routing configuration, it fills `IJsonApiRequest`, an injectable object that contains JSON:API-related information about the request being processed.

It is possible to replace the built-in middleware components by configuring the IoC container and by configuring `MvcOptions`.
It is possible to replace the built-in middleware components by configuring the IoC container and by configuring `MvcOptions`.

## Configuring the IoC container
## Configuring the IoC container

The following example replaces the internal exception filter with a custom implementation.

Expand Down
4 changes: 2 additions & 2 deletions docs/usage/extensibility/resource-definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ For various reasons (see examples below) you may need to change parts of the que
`JsonApiResourceDefinition<TResource, TId>` (which is an empty implementation of `IResourceDefinition<TResource, TId>`) provides overridable methods that pass you the result of query string parameter parsing.
The value returned by you determines what will be used to execute the query.

An intermediate format (`QueryExpression` and derived types) is used, which enables us to separate JSON:API implementation
An intermediate format (`QueryExpression` and derived types) is used, which enables us to separate JSON:API implementation
from Entity Framework Core `IQueryable` execution.

### Excluding fields
Expand Down Expand Up @@ -220,7 +220,7 @@ You can define additional query string parameters with the LINQ expression that
If the key is present in a query string, the supplied LINQ expression will be added to the database query.

> [!NOTE]
> This directly influences the Entity Framework Core `IQueryable`. As opposed to using `OnApplyFilter`, this enables the full range of Entity Framework Core operators.
> This directly influences the Entity Framework Core `IQueryable`. As opposed to using `OnApplyFilter`, this enables the full range of Entity Framework Core operators.
But it only works on primary resource endpoints (for example: /articles, but not on /blogs/1/articles or /blogs?include=articles).

```c#
Expand Down
2 changes: 1 addition & 1 deletion docs/usage/reading/filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ The next request returns all customers that have orders -or- whose last name is
GET /customers?filter=has(orders)&filter=equals(lastName,'Smith') HTTP/1.1
```

Aside from filtering on the resource being requested (which would be blogs in /blogs and articles in /blogs/1/articles),
Aside from filtering on the resource being requested (which would be blogs in /blogs and articles in /blogs/1/articles),
filtering on to-many relationships can be done using bracket notation:

```http
Expand Down
2 changes: 1 addition & 1 deletion docs/usage/resource-graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ There are three ways the resource graph can be created:
2. Specifying an entire DbContext
3. Manually specifying each resource

It is also possible to combine the three of them at once. Be aware that some configuration might overlap,
It is also possible to combine the three of them at once. Be aware that some configuration might overlap,
for example one could manually add a resource to the graph which is also auto-discovered. In such a scenario, the configuration
is prioritized by the list above in descending order.

Expand Down
4 changes: 2 additions & 2 deletions docs/usage/resources/relationships.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,8 @@ public class TodoItem : Identifiable<int>

_since v5.1_

Default JSON:API relationship capabilities are specified in
@JsonApiDotNetCore.Configuration.JsonApiOptions#JsonApiDotNetCore_Configuration_JsonApiOptions_DefaultHasOneCapabilities and
Default JSON:API relationship capabilities are specified in
@JsonApiDotNetCore.Configuration.JsonApiOptions#JsonApiDotNetCore_Configuration_JsonApiOptions_DefaultHasOneCapabilities and
@JsonApiDotNetCore.Configuration.JsonApiOptions#JsonApiDotNetCore_Configuration_JsonApiOptions_DefaultHasManyCapabilities:

```c#
Expand Down
2 changes: 1 addition & 1 deletion docs/usage/writing/bulk-batch-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public sealed class OperationsController : JsonApiOperationsController
}
```

> [!IMPORTANT]
> [!IMPORTANT]
> Since v5.6.0, the set of exposed operations is based on
> [`GenerateControllerEndpoints` usage](~/usage/extensibility/controllers.md#resource-access-control).
> Earlier versions always exposed all operations for all resource types.
Expand Down
26 changes: 17 additions & 9 deletions src/JsonApiDotNetCore/Queries/QueryLayerComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,18 +96,25 @@ public QueryLayerComposer(IEnumerable<IQueryConstraintProvider> constraintProvid
// @formatter:wrap_chained_method_calls restore

FilterExpression? primaryFilter = GetFilter(Array.Empty<QueryExpression>(), hasManyRelationship.LeftType);
FilterExpression? secondaryFilter = GetFilter(filtersInSecondaryScope, hasManyRelationship.RightType);

FilterExpression inverseFilter = GetInverseRelationshipFilter(primaryId, hasManyRelationship, inverseRelationship);
if (primaryFilter != null && inverseRelationship is HasOneAttribute)
{
// We can't lift the field chains in a primary filter, because there's no way for a custom filter expression to express
// the scope of its chains. See https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1671.
return null;
}

FilterExpression? secondaryFilter = GetFilter(filtersInSecondaryScope, hasManyRelationship.RightType);
FilterExpression inverseFilter = GetInverseRelationshipFilter(primaryId, primaryFilter, hasManyRelationship, inverseRelationship);

return LogicalExpression.Compose(LogicalOperator.And, inverseFilter, primaryFilter, secondaryFilter);
return LogicalExpression.Compose(LogicalOperator.And, inverseFilter, secondaryFilter);
}

private static FilterExpression GetInverseRelationshipFilter<TId>([DisallowNull] TId primaryId, HasManyAttribute relationship,
RelationshipAttribute inverseRelationship)
private static FilterExpression GetInverseRelationshipFilter<TId>([DisallowNull] TId primaryId, FilterExpression? primaryFilter,
HasManyAttribute relationship, RelationshipAttribute inverseRelationship)
{
return inverseRelationship is HasManyAttribute hasManyInverseRelationship
? GetInverseHasManyRelationshipFilter(primaryId, relationship, hasManyInverseRelationship)
? GetInverseHasManyRelationshipFilter(primaryId, primaryFilter, relationship, hasManyInverseRelationship)
: GetInverseHasOneRelationshipFilter(primaryId, relationship, (HasOneAttribute)inverseRelationship);
}

Expand All @@ -120,14 +127,15 @@ private static ComparisonExpression GetInverseHasOneRelationshipFilter<TId>([Dis
return new ComparisonExpression(ComparisonOperator.Equals, idChain, new LiteralConstantExpression(primaryId));
}

private static HasExpression GetInverseHasManyRelationshipFilter<TId>([DisallowNull] TId primaryId, HasManyAttribute relationship,
HasManyAttribute inverseRelationship)
private static HasExpression GetInverseHasManyRelationshipFilter<TId>([DisallowNull] TId primaryId, FilterExpression? primaryFilter,
HasManyAttribute relationship, HasManyAttribute inverseRelationship)
{
AttrAttribute idAttribute = GetIdAttribute(relationship.LeftType);
var idChain = new ResourceFieldChainExpression(ImmutableArray.Create<ResourceFieldAttribute>(idAttribute));
var idComparison = new ComparisonExpression(ComparisonOperator.Equals, idChain, new LiteralConstantExpression(primaryId));

return new HasExpression(new ResourceFieldChainExpression(inverseRelationship), idComparison);
FilterExpression filter = LogicalExpression.Compose(LogicalOperator.And, idComparison, primaryFilter)!;
return new HasExpression(new ResourceFieldChainExpression(inverseRelationship), filter);
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,11 @@ public override ResourceObject Read(ref Utf8JsonReader reader, Type typeToConver
}
}

attributes.Add(attributeName, attributeValue);
attributes[attributeName] = attributeValue;
}
else
{
attributes.Add(attributeName, null);
attributes[attributeName] = null;
reader.Skip();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
using JetBrains.Annotations;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;

namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceDefinitions.Reading;

[UsedImplicitly(ImplicitUseTargetFlags.Members)]
[Resource(ControllerNamespace = "JsonApiDotNetCoreTests.IntegrationTests.ResourceDefinitions.Reading")]
public sealed class Constellation : Identifiable<int>
{
[Attr]
public string Name { get; set; } = null!;

[Attr]
[Required]
public Season? VisibleDuring { get; set; }

[HasMany]
public ISet<Star> Stars { get; set; } = new HashSet<Star>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Queries.Expressions;
using JsonApiDotNetCore.Resources.Annotations;

namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceDefinitions.Reading;

[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
public sealed class ConstellationDefinition(
IResourceGraph resourceGraph, IClientSettingsProvider clientSettingsProvider, ResourceDefinitionHitCounter hitCounter)
: HitCountingResourceDefinition<Constellation, int>(resourceGraph, hitCounter)
{
private readonly IClientSettingsProvider _clientSettingsProvider = clientSettingsProvider;

protected override ResourceDefinitionExtensibilityPoints ExtensibilityPointsToTrack => ResourceDefinitionExtensibilityPoints.Reading;

public override FilterExpression? OnApplyFilter(FilterExpression? existingFilter)
{
FilterExpression? baseFilter = base.OnApplyFilter(existingFilter);

if (_clientSettingsProvider.AreConstellationsVisibleDuringWinterHidden)
{
AttrAttribute visibleDuringAttribute = ResourceType.GetAttributeByPropertyName(nameof(Constellation.VisibleDuring));
var visibleDuringChain = new ResourceFieldChainExpression(visibleDuringAttribute);
var visibleDuringComparison = new ComparisonExpression(ComparisonOperator.Equals, visibleDuringChain, new LiteralConstantExpression(Season.Winter));
var notVisibleDuringComparison = new NotExpression(visibleDuringComparison);

return LogicalExpression.Compose(LogicalOperator.And, baseFilter, notVisibleDuringComparison);
}

return baseFilter;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ namespace JsonApiDotNetCoreTests.IntegrationTests.ResourceDefinitions.Reading;

public interface IClientSettingsProvider
{
bool AreVeryLargeStarsHidden { get; }
bool AreConstellationsVisibleDuringWinterHidden { get; }
bool IsIncludePlanetMoonsBlocked { get; }
bool ArePlanetsWithPrivateNameHidden { get; }
bool IsStarGivingLightToMoonAutoIncluded { get; }
Expand Down
Loading
Loading