- The goal of this library is to simplify the development of APIs that leverage the full range of features provided by the JSON:API specification. + The goal of this library is to simplify the development of APIs that leverage the full range of features + provided by the JSON:API specification. You just need to focus on defining the resources and implementing your custom business logic.
We strive to eliminate as much boilerplate as possible by offering out-of-the-box features such as sorting, filtering and pagination.
The following features are supported, from HTTP all the way down to the database
Perform compound filtering using the filter
query string parameter
Order resources on one or multiple attributes using the sort
query string parameter
Leverage the benefits of paginated resources with the page
query string parameter
Side-load related resources of nested relationships using the include
query string parameter
Configure permissions, such as view/create/change/sort/filter of attributes and relationships
+Configure permissions, such as viewing, creating, modifying, sorting and filtering of attributes and relationships
Validate incoming requests using built-in ASP.NET Core ModelState
validation, which works seamlessly with partial updates
Validate incoming requests using built-in ASP.NET Model Validation, which works seamlessly with partial updates
Use various extensibility points to intercept and run custom code, besides just model annotations
#nullable enable
@@ -179,16 +183,16 @@ Resource
-
+
Request
GET /articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields[articles]=title,summary&include=author HTTP/1.1
-
+
-
+
Response
{
@@ -259,15 +263,15 @@ Response
Sponsors
-
-
+
+
-
-
+
+
From 06d6b18056a920b1570ec9e5336d60076c2d53a3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 Mar 2024 11:26:35 +0100
Subject: [PATCH 19/63] Bump Microsoft.CodeAnalysis.CSharp from 4.1.0 to 4.9.2
(#1490)
Bumps [Microsoft.CodeAnalysis.CSharp](https://github.com/dotnet/roslyn) from 4.1.0 to 4.9.2.
- [Release notes](https://github.com/dotnet/roslyn/releases)
- [Changelog](https://github.com/dotnet/roslyn/blob/main/docs/Breaking%20API%20Changes.md)
- [Commits](https://github.com/dotnet/roslyn/commits)
---
updated-dependencies:
- dependency-name: Microsoft.CodeAnalysis.CSharp
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
package-versions.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package-versions.props b/package-versions.props
index 07bbfed960..e894e80861 100644
--- a/package-versions.props
+++ b/package-versions.props
@@ -8,7 +8,7 @@
0.13.*
35.2.*
- 4.8.*
+ 4.9.*
6.0.*
2.1.*
6.12.*
From f955b08321250a43c7ab41e2f4e0af493f3a4855 Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Tue, 5 Mar 2024 18:09:48 +0100
Subject: [PATCH 20/63] Use read-only interfaces for querying EF Core model
---
.../DataModel/FromEntitiesDataModelService.cs | 14 +++++++-------
.../QueryLayerToLinqConverter.cs | 6 +++---
.../QueryClauseBuilderContext.cs | 4 ++--
.../QueryableBuilderContext.cs | 10 +++++-----
.../QueryableBuilding/SelectClauseBuilder.cs | 19 ++++++++++---------
5 files changed, 27 insertions(+), 26 deletions(-)
diff --git a/src/Examples/DapperExample/TranslationToSql/DataModel/FromEntitiesDataModelService.cs b/src/Examples/DapperExample/TranslationToSql/DataModel/FromEntitiesDataModelService.cs
index b0ba38f3a5..711ad8517c 100644
--- a/src/Examples/DapperExample/TranslationToSql/DataModel/FromEntitiesDataModelService.cs
+++ b/src/Examples/DapperExample/TranslationToSql/DataModel/FromEntitiesDataModelService.cs
@@ -38,12 +38,12 @@ public void Initialize(DbContext dbContext)
Initialize();
}
- private void ScanForeignKeys(IModel entityModel)
+ private void ScanForeignKeys(IReadOnlyModel entityModel)
{
foreach (RelationshipAttribute relationship in ResourceGraph.GetResourceTypes().SelectMany(resourceType => resourceType.Relationships))
{
- IEntityType? leftEntityType = entityModel.FindEntityType(relationship.LeftType.ClrType);
- INavigation? navigation = leftEntityType?.FindNavigation(relationship.Property.Name);
+ IReadOnlyEntityType? leftEntityType = entityModel.FindEntityType(relationship.LeftType.ClrType);
+ IReadOnlyNavigation? navigation = leftEntityType?.FindNavigation(relationship.Property.Name);
if (navigation != null)
{
@@ -57,7 +57,7 @@ private void ScanForeignKeys(IModel entityModel)
}
}
- private void ScanColumnNullability(IModel entityModel)
+ private void ScanColumnNullability(IReadOnlyModel entityModel)
{
foreach (ResourceType resourceType in ResourceGraph.GetResourceTypes())
{
@@ -65,15 +65,15 @@ private void ScanColumnNullability(IModel entityModel)
}
}
- private void ScanColumnNullability(ResourceType resourceType, IModel entityModel)
+ private void ScanColumnNullability(ResourceType resourceType, IReadOnlyModel entityModel)
{
- IEntityType? entityType = entityModel.FindEntityType(resourceType.ClrType);
+ IReadOnlyEntityType? entityType = entityModel.FindEntityType(resourceType.ClrType);
if (entityType != null)
{
foreach (AttrAttribute attribute in resourceType.Attributes)
{
- IProperty? property = entityType.FindProperty(attribute.Property.Name);
+ IReadOnlyProperty? property = entityType.FindProperty(attribute.Property.Name);
if (property != null)
{
diff --git a/src/Examples/NoEntityFrameworkExample/QueryLayerToLinqConverter.cs b/src/Examples/NoEntityFrameworkExample/QueryLayerToLinqConverter.cs
index 29d5f999e9..a67a694aef 100644
--- a/src/Examples/NoEntityFrameworkExample/QueryLayerToLinqConverter.cs
+++ b/src/Examples/NoEntityFrameworkExample/QueryLayerToLinqConverter.cs
@@ -7,9 +7,9 @@
namespace NoEntityFrameworkExample;
-internal sealed class QueryLayerToLinqConverter(IModel model, IQueryableBuilder queryableBuilder)
+internal sealed class QueryLayerToLinqConverter(IReadOnlyModel entityModel, IQueryableBuilder queryableBuilder)
{
- private readonly IModel _model = model;
+ private readonly IReadOnlyModel _entityModel = entityModel;
private readonly IQueryableBuilder _queryableBuilder = queryableBuilder;
public IEnumerable ApplyQueryLayer(QueryLayer queryLayer, IEnumerable resources)
@@ -21,7 +21,7 @@ public IEnumerable ApplyQueryLayer(QueryLayer queryLayer,
// Convert QueryLayer into LINQ expression.
IQueryable source = ((IEnumerable)resources).AsQueryable();
- var context = QueryableBuilderContext.CreateRoot(source, typeof(Enumerable), _model, null);
+ var context = QueryableBuilderContext.CreateRoot(source, typeof(Enumerable), _entityModel, null);
Expression expression = _queryableBuilder.ApplyQuery(queryLayer, context);
// Insert null checks to prevent a NullReferenceException during execution of expressions such as:
diff --git a/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryClauseBuilderContext.cs b/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryClauseBuilderContext.cs
index 42dcf80428..05cccf7943 100644
--- a/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryClauseBuilderContext.cs
+++ b/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryClauseBuilderContext.cs
@@ -29,7 +29,7 @@ public sealed class QueryClauseBuilderContext
///
/// The Entity Framework Core entity model.
///
- public IModel EntityModel { get; }
+ public IReadOnlyModel EntityModel { get; }
///
/// Used to produce unique names for lambda parameters.
@@ -51,7 +51,7 @@ public sealed class QueryClauseBuilderContext
///
public object? State { get; }
- public QueryClauseBuilderContext(Expression source, ResourceType resourceType, Type extensionType, IModel entityModel,
+ public QueryClauseBuilderContext(Expression source, ResourceType resourceType, Type extensionType, IReadOnlyModel entityModel,
LambdaScopeFactory lambdaScopeFactory, LambdaScope lambdaScope, IQueryableBuilder queryableBuilder, object? state)
{
ArgumentGuard.NotNull(source);
diff --git a/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryableBuilderContext.cs b/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryableBuilderContext.cs
index 4659cca875..358990fdc7 100644
--- a/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryableBuilderContext.cs
+++ b/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryableBuilderContext.cs
@@ -29,7 +29,7 @@ public sealed class QueryableBuilderContext
///
/// The Entity Framework Core entity model.
///
- public IModel EntityModel { get; }
+ public IReadOnlyModel EntityModel { get; }
///
/// Used to produce unique names for lambda parameters.
@@ -41,7 +41,7 @@ public sealed class QueryableBuilderContext
///
public object? State { get; }
- public QueryableBuilderContext(Expression source, Type elementType, Type extensionType, IModel entityModel, LambdaScopeFactory lambdaScopeFactory,
+ public QueryableBuilderContext(Expression source, Type elementType, Type extensionType, IReadOnlyModel entityModel, LambdaScopeFactory lambdaScopeFactory,
object? state)
{
ArgumentGuard.NotNull(source);
@@ -58,15 +58,15 @@ public QueryableBuilderContext(Expression source, Type elementType, Type extensi
State = state;
}
- public static QueryableBuilderContext CreateRoot(IQueryable source, Type extensionType, IModel model, object? state)
+ public static QueryableBuilderContext CreateRoot(IQueryable source, Type extensionType, IReadOnlyModel entityModel, object? state)
{
ArgumentGuard.NotNull(source);
ArgumentGuard.NotNull(extensionType);
- ArgumentGuard.NotNull(model);
+ ArgumentGuard.NotNull(entityModel);
var lambdaScopeFactory = new LambdaScopeFactory();
- return new QueryableBuilderContext(source.Expression, source.ElementType, extensionType, model, lambdaScopeFactory, state);
+ return new QueryableBuilderContext(source.Expression, source.ElementType, extensionType, entityModel, lambdaScopeFactory, state);
}
public QueryClauseBuilderContext CreateClauseContext(IQueryableBuilder queryableBuilder, Expression source, ResourceType resourceType,
diff --git a/src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs
index d1113946ad..c67b785d89 100644
--- a/src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs
+++ b/src/JsonApiDotNetCore/Queries/QueryableBuilding/SelectClauseBuilder.cs
@@ -40,8 +40,8 @@ public virtual Expression ApplySelect(FieldSelection selection, QueryClauseBuild
private Expression CreateLambdaBodyInitializer(FieldSelection selection, ResourceType resourceType, LambdaScope lambdaScope,
bool lambdaAccessorRequiresTestForNull, QueryClauseBuilderContext context)
{
- IEntityType entityType = context.EntityModel.FindEntityType(resourceType.ClrType)!;
- IEntityType[] concreteEntityTypes = entityType.GetConcreteDerivedTypesInclusive().ToArray();
+ IReadOnlyEntityType entityType = context.EntityModel.FindEntityType(resourceType.ClrType)!;
+ IReadOnlyEntityType[] concreteEntityTypes = entityType.GetConcreteDerivedTypesInclusive().ToArray();
Expression bodyInitializer = concreteEntityTypes.Length > 1
? CreateLambdaBodyInitializerForTypeHierarchy(selection, resourceType, concreteEntityTypes, lambdaScope, context)
@@ -56,12 +56,12 @@ private Expression CreateLambdaBodyInitializer(FieldSelection selection, Resourc
}
private Expression CreateLambdaBodyInitializerForTypeHierarchy(FieldSelection selection, ResourceType baseResourceType,
- IEnumerable concreteEntityTypes, LambdaScope lambdaScope, QueryClauseBuilderContext context)
+ IEnumerable concreteEntityTypes, LambdaScope lambdaScope, QueryClauseBuilderContext context)
{
IReadOnlySet resourceTypes = selection.GetResourceTypes();
Expression rootCondition = lambdaScope.Accessor;
- foreach (IEntityType entityType in concreteEntityTypes)
+ foreach (IReadOnlyEntityType entityType in concreteEntityTypes)
{
ResourceType? resourceType = resourceTypes.SingleOrDefault(type => type.ClrType == entityType.ClrType);
@@ -115,7 +115,7 @@ private Expression CreateLambdaBodyInitializerForSingleType(FieldSelection selec
}
private static ICollection ToPropertySelectors(FieldSelectors fieldSelectors, ResourceType resourceType, Type elementType,
- IModel entityModel)
+ IReadOnlyModel entityModel)
{
var propertySelectors = new Dictionary();
@@ -134,17 +134,18 @@ private static ICollection ToPropertySelectors(FieldSelectors
return propertySelectors.Values;
}
- private static void IncludeAllScalarProperties(Type elementType, Dictionary propertySelectors, IModel entityModel)
+ private static void IncludeAllScalarProperties(Type elementType, Dictionary propertySelectors, IReadOnlyModel entityModel)
{
- IEntityType entityType = entityModel.GetEntityTypes().Single(type => type.ClrType == elementType);
+ IReadOnlyEntityType entityType = entityModel.GetEntityTypes().Single(type => type.ClrType == elementType);
- foreach (IProperty property in entityType.GetProperties().Where(property => !property.IsShadowProperty()))
+ foreach (IReadOnlyProperty property in entityType.GetProperties().Where(property => !property.IsShadowProperty()))
{
var propertySelector = new PropertySelector(property.PropertyInfo!);
IncludeWritableProperty(propertySelector, propertySelectors);
}
- foreach (INavigation navigation in entityType.GetNavigations().Where(navigation => navigation.ForeignKey.IsOwnership && !navigation.IsShadowProperty()))
+ foreach (IReadOnlyNavigation navigation in entityType.GetNavigations()
+ .Where(navigation => navigation.ForeignKey.IsOwnership && !navigation.IsShadowProperty()))
{
var propertySelector = new PropertySelector(navigation.PropertyInfo!);
IncludeWritableProperty(propertySelector, propertySelectors);
From 3bf91575ffb7779170552be6fad330aaad5475f0 Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Tue, 5 Mar 2024 18:19:19 +0100
Subject: [PATCH 21/63] Use ModelBuilder in sample to not depend on EF Core
internals, build once at startup
---
.../Data/InMemoryModel.cs | 25 -------------
.../Data/ResourceGraphExtensions.cs | 32 ++++++++++++++++
.../NoEntityFrameworkExample/Program.cs | 7 ++++
.../InMemoryResourceRepository.cs | 17 +++------
.../Repositories/PersonRepository.cs | 5 ++-
.../Repositories/TagRepository.cs | 5 ++-
.../Repositories/TodoItemRepository.cs | 5 ++-
.../Services/InMemoryResourceService.cs | 37 ++++++-------------
.../Services/TodoItemService.cs | 5 ++-
9 files changed, 68 insertions(+), 70 deletions(-)
delete mode 100644 src/Examples/NoEntityFrameworkExample/Data/InMemoryModel.cs
create mode 100644 src/Examples/NoEntityFrameworkExample/Data/ResourceGraphExtensions.cs
diff --git a/src/Examples/NoEntityFrameworkExample/Data/InMemoryModel.cs b/src/Examples/NoEntityFrameworkExample/Data/InMemoryModel.cs
deleted file mode 100644
index c81aa07b8f..0000000000
--- a/src/Examples/NoEntityFrameworkExample/Data/InMemoryModel.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System.Reflection;
-using JsonApiDotNetCore.Configuration;
-using Microsoft.EntityFrameworkCore.Metadata;
-
-namespace NoEntityFrameworkExample.Data;
-
-internal sealed class InMemoryModel : RuntimeModel
-{
- public InMemoryModel(IResourceGraph resourceGraph)
- {
- foreach (ResourceType resourceType in resourceGraph.GetResourceTypes())
- {
- RuntimeEntityType entityType = AddEntityType(resourceType.ClrType.FullName!, resourceType.ClrType);
- SetEntityProperties(entityType, resourceType);
- }
- }
-
- private static void SetEntityProperties(RuntimeEntityType entityType, ResourceType resourceType)
- {
- foreach (PropertyInfo property in resourceType.ClrType.GetProperties())
- {
- entityType.AddProperty(property.Name, property.PropertyType, property);
- }
- }
-}
diff --git a/src/Examples/NoEntityFrameworkExample/Data/ResourceGraphExtensions.cs b/src/Examples/NoEntityFrameworkExample/Data/ResourceGraphExtensions.cs
new file mode 100644
index 0000000000..ff35f0ab0d
--- /dev/null
+++ b/src/Examples/NoEntityFrameworkExample/Data/ResourceGraphExtensions.cs
@@ -0,0 +1,32 @@
+using System.Reflection;
+using JsonApiDotNetCore.Configuration;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace NoEntityFrameworkExample.Data;
+
+internal static class ResourceGraphExtensions
+{
+ public static IReadOnlyModel ToEntityModel(this IResourceGraph resourceGraph)
+ {
+ var modelBuilder = new ModelBuilder();
+
+ foreach (ResourceType resourceType in resourceGraph.GetResourceTypes())
+ {
+ IncludeResourceType(resourceType, modelBuilder);
+ }
+
+ return modelBuilder.Model;
+ }
+
+ private static void IncludeResourceType(ResourceType resourceType, ModelBuilder builder)
+ {
+ EntityTypeBuilder entityTypeBuilder = builder.Entity(resourceType.ClrType);
+
+ foreach (PropertyInfo property in resourceType.ClrType.GetProperties())
+ {
+ entityTypeBuilder.Property(property.PropertyType, property.Name);
+ }
+ }
+}
diff --git a/src/Examples/NoEntityFrameworkExample/Program.cs b/src/Examples/NoEntityFrameworkExample/Program.cs
index 8546e939e8..f21d116e5f 100755
--- a/src/Examples/NoEntityFrameworkExample/Program.cs
+++ b/src/Examples/NoEntityFrameworkExample/Program.cs
@@ -1,5 +1,6 @@
using JsonApiDotNetCore.Configuration;
using NoEntityFrameworkExample;
+using NoEntityFrameworkExample.Data;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
@@ -20,6 +21,12 @@
#endif
}, discovery => discovery.AddCurrentAssembly());
+builder.Services.AddSingleton(serviceProvider =>
+{
+ var resourceGraph = serviceProvider.GetRequiredService();
+ return resourceGraph.ToEntityModel();
+});
+
WebApplication app = builder.Build();
// Configure the HTTP request pipeline.
diff --git a/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs b/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs
index 243b484a9b..9d0852ad7f 100644
--- a/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs
+++ b/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs
@@ -4,7 +4,7 @@
using JsonApiDotNetCore.Queries.QueryableBuilding;
using JsonApiDotNetCore.Repositories;
using JsonApiDotNetCore.Resources;
-using NoEntityFrameworkExample.Data;
+using Microsoft.EntityFrameworkCore.Metadata;
namespace NoEntityFrameworkExample.Repositories;
@@ -19,19 +19,12 @@ namespace NoEntityFrameworkExample.Repositories;
///
/// The resource identifier type.
///
-public abstract class InMemoryResourceRepository : IResourceReadRepository
+public abstract class InMemoryResourceRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder, IReadOnlyModel entityModel)
+ : IResourceReadRepository
where TResource : class, IIdentifiable
{
- private readonly ResourceType _resourceType;
- private readonly QueryLayerToLinqConverter _queryLayerToLinqConverter;
-
- protected InMemoryResourceRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder)
- {
- _resourceType = resourceGraph.GetResourceType();
-
- var model = new InMemoryModel(resourceGraph);
- _queryLayerToLinqConverter = new QueryLayerToLinqConverter(model, queryableBuilder);
- }
+ private readonly ResourceType _resourceType = resourceGraph.GetResourceType();
+ private readonly QueryLayerToLinqConverter _queryLayerToLinqConverter = new(entityModel, queryableBuilder);
///
public Task> GetAsync(QueryLayer queryLayer, CancellationToken cancellationToken)
diff --git a/src/Examples/NoEntityFrameworkExample/Repositories/PersonRepository.cs b/src/Examples/NoEntityFrameworkExample/Repositories/PersonRepository.cs
index 897af592b7..8e2725379c 100644
--- a/src/Examples/NoEntityFrameworkExample/Repositories/PersonRepository.cs
+++ b/src/Examples/NoEntityFrameworkExample/Repositories/PersonRepository.cs
@@ -1,14 +1,15 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Queries.QueryableBuilding;
+using Microsoft.EntityFrameworkCore.Metadata;
using NoEntityFrameworkExample.Data;
using NoEntityFrameworkExample.Models;
namespace NoEntityFrameworkExample.Repositories;
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
-public sealed class PersonRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder)
- : InMemoryResourceRepository(resourceGraph, queryableBuilder)
+public sealed class PersonRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder, IReadOnlyModel entityModel)
+ : InMemoryResourceRepository(resourceGraph, queryableBuilder, entityModel)
{
protected override IEnumerable GetDataSource()
{
diff --git a/src/Examples/NoEntityFrameworkExample/Repositories/TagRepository.cs b/src/Examples/NoEntityFrameworkExample/Repositories/TagRepository.cs
index 30658fb68d..81a28ed6bc 100644
--- a/src/Examples/NoEntityFrameworkExample/Repositories/TagRepository.cs
+++ b/src/Examples/NoEntityFrameworkExample/Repositories/TagRepository.cs
@@ -1,14 +1,15 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Queries.QueryableBuilding;
+using Microsoft.EntityFrameworkCore.Metadata;
using NoEntityFrameworkExample.Data;
using NoEntityFrameworkExample.Models;
namespace NoEntityFrameworkExample.Repositories;
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
-public sealed class TagRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder)
- : InMemoryResourceRepository(resourceGraph, queryableBuilder)
+public sealed class TagRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder, IReadOnlyModel entityModel)
+ : InMemoryResourceRepository(resourceGraph, queryableBuilder, entityModel)
{
protected override IEnumerable GetDataSource()
{
diff --git a/src/Examples/NoEntityFrameworkExample/Repositories/TodoItemRepository.cs b/src/Examples/NoEntityFrameworkExample/Repositories/TodoItemRepository.cs
index 41774b0c8f..335d7c5c5a 100644
--- a/src/Examples/NoEntityFrameworkExample/Repositories/TodoItemRepository.cs
+++ b/src/Examples/NoEntityFrameworkExample/Repositories/TodoItemRepository.cs
@@ -1,14 +1,15 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Queries.QueryableBuilding;
+using Microsoft.EntityFrameworkCore.Metadata;
using NoEntityFrameworkExample.Data;
using NoEntityFrameworkExample.Models;
namespace NoEntityFrameworkExample.Repositories;
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
-public sealed class TodoItemRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder)
- : InMemoryResourceRepository(resourceGraph, queryableBuilder)
+public sealed class TodoItemRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder, IReadOnlyModel entityModel)
+ : InMemoryResourceRepository(resourceGraph, queryableBuilder, entityModel)
{
protected override IEnumerable GetDataSource()
{
diff --git a/src/Examples/NoEntityFrameworkExample/Services/InMemoryResourceService.cs b/src/Examples/NoEntityFrameworkExample/Services/InMemoryResourceService.cs
index 67c7a4138c..e9b37560fc 100644
--- a/src/Examples/NoEntityFrameworkExample/Services/InMemoryResourceService.cs
+++ b/src/Examples/NoEntityFrameworkExample/Services/InMemoryResourceService.cs
@@ -7,7 +7,7 @@
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Services;
-using NoEntityFrameworkExample.Data;
+using Microsoft.EntityFrameworkCore.Metadata;
namespace NoEntityFrameworkExample.Services;
@@ -30,32 +30,19 @@ namespace NoEntityFrameworkExample.Services;
///
/// The resource identifier type.
///
-public abstract class InMemoryResourceService : IResourceQueryService
+public abstract class InMemoryResourceService(
+ IJsonApiOptions options, IResourceGraph resourceGraph, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext,
+ IEnumerable constraintProviders, IQueryableBuilder queryableBuilder, IReadOnlyModel entityModel,
+ ILoggerFactory loggerFactory) : IResourceQueryService
where TResource : class, IIdentifiable
{
- private readonly IJsonApiOptions _options;
- private readonly IQueryLayerComposer _queryLayerComposer;
- private readonly IPaginationContext _paginationContext;
- private readonly IEnumerable _constraintProviders;
- private readonly ILogger> _logger;
- private readonly ResourceType _resourceType;
- private readonly QueryLayerToLinqConverter _queryLayerToLinqConverter;
-
- protected InMemoryResourceService(IJsonApiOptions options, IResourceGraph resourceGraph, IQueryLayerComposer queryLayerComposer,
- IPaginationContext paginationContext, IEnumerable constraintProviders, IQueryableBuilder queryableBuilder,
- ILoggerFactory loggerFactory)
- {
- _options = options;
- _queryLayerComposer = queryLayerComposer;
- _paginationContext = paginationContext;
- _constraintProviders = constraintProviders;
-
- _logger = loggerFactory.CreateLogger>();
- _resourceType = resourceGraph.GetResourceType();
-
- var model = new InMemoryModel(resourceGraph);
- _queryLayerToLinqConverter = new QueryLayerToLinqConverter(model, queryableBuilder);
- }
+ private readonly IJsonApiOptions _options = options;
+ private readonly IQueryLayerComposer _queryLayerComposer = queryLayerComposer;
+ private readonly IPaginationContext _paginationContext = paginationContext;
+ private readonly IEnumerable _constraintProviders = constraintProviders;
+ private readonly ILogger> _logger = loggerFactory.CreateLogger>();
+ private readonly ResourceType _resourceType = resourceGraph.GetResourceType();
+ private readonly QueryLayerToLinqConverter _queryLayerToLinqConverter = new(entityModel, queryableBuilder);
///
public Task> GetAsync(CancellationToken cancellationToken)
diff --git a/src/Examples/NoEntityFrameworkExample/Services/TodoItemService.cs b/src/Examples/NoEntityFrameworkExample/Services/TodoItemService.cs
index 294d23978c..d38cca9c94 100644
--- a/src/Examples/NoEntityFrameworkExample/Services/TodoItemService.cs
+++ b/src/Examples/NoEntityFrameworkExample/Services/TodoItemService.cs
@@ -3,6 +3,7 @@
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Queries.QueryableBuilding;
using JsonApiDotNetCore.Resources;
+using Microsoft.EntityFrameworkCore.Metadata;
using NoEntityFrameworkExample.Data;
using NoEntityFrameworkExample.Models;
@@ -11,8 +12,8 @@ namespace NoEntityFrameworkExample.Services;
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
public sealed class TodoItemService(
IJsonApiOptions options, IResourceGraph resourceGraph, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext,
- IEnumerable constraintProviders, IQueryableBuilder queryableBuilder, ILoggerFactory loggerFactory)
- : InMemoryResourceService(options, resourceGraph, queryLayerComposer, paginationContext, constraintProviders, queryableBuilder,
+ IEnumerable constraintProviders, IQueryableBuilder queryableBuilder, IReadOnlyModel entityModel, ILoggerFactory loggerFactory)
+ : InMemoryResourceService(options, resourceGraph, queryLayerComposer, paginationContext, constraintProviders, queryableBuilder, entityModel,
loggerFactory)
{
protected override IEnumerable GetDataSource(ResourceType resourceType)
From 6db37deca2131e303bd781599acfe44791d93114 Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Sun, 10 Mar 2024 16:59:18 +0100
Subject: [PATCH 22/63] Enable setting the "describedby" top-level link by
implementing IDocumentDescriptionLinkProvider
---
.../JsonApiApplicationBuilder.cs | 1 +
.../IDocumentDescriptionLinkProvider.cs | 17 +++
.../Serialization/Response/LinkBuilder.cs | 16 ++-
.../NoDocumentDescriptionLinkProvider.cs | 15 +++
.../Serialization/Response/UriNormalizer.cs | 80 ++++++++++++++
.../Links/AbsoluteLinksWithNamespaceTests.cs | 8 ++
.../AbsoluteLinksWithoutNamespaceTests.cs | 8 ++
.../Links/DocumentDescriptionLinkTests.cs | 100 ++++++++++++++++++
.../Links/RelativeLinksWithNamespaceTests.cs | 8 ++
.../RelativeLinksWithoutNamespaceTests.cs | 8 ++
.../UnitTests/Links/LinkInclusionTests.cs | 15 ++-
.../UnitTests/Links/UriNormalizerTests.cs | 78 ++++++++++++++
12 files changed, 350 insertions(+), 4 deletions(-)
create mode 100644 src/JsonApiDotNetCore/Serialization/Response/IDocumentDescriptionLinkProvider.cs
create mode 100644 src/JsonApiDotNetCore/Serialization/Response/NoDocumentDescriptionLinkProvider.cs
create mode 100644 src/JsonApiDotNetCore/Serialization/Response/UriNormalizer.cs
create mode 100644 test/JsonApiDotNetCoreTests/IntegrationTests/Links/DocumentDescriptionLinkTests.cs
create mode 100644 test/JsonApiDotNetCoreTests/UnitTests/Links/UriNormalizerTests.cs
diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
index 2004178ccd..2973a664f6 100644
--- a/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
+++ b/src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
@@ -168,6 +168,7 @@ public void ConfigureServiceContainer(ICollection dbContextTypes)
_services.TryAddScoped();
_services.TryAddScoped();
_services.TryAddScoped();
+ _services.TryAddSingleton();
}
private void AddMiddlewareLayer()
diff --git a/src/JsonApiDotNetCore/Serialization/Response/IDocumentDescriptionLinkProvider.cs b/src/JsonApiDotNetCore/Serialization/Response/IDocumentDescriptionLinkProvider.cs
new file mode 100644
index 0000000000..54a5ae36b3
--- /dev/null
+++ b/src/JsonApiDotNetCore/Serialization/Response/IDocumentDescriptionLinkProvider.cs
@@ -0,0 +1,17 @@
+using JsonApiDotNetCore.Configuration;
+
+namespace JsonApiDotNetCore.Serialization.Response;
+
+///
+/// Provides the value for the "describedby" link in https://jsonapi.org/format/#document-top-level.
+///
+public interface IDocumentDescriptionLinkProvider
+{
+ ///
+ /// Gets the URL for the "describedby" link, or null when unavailable.
+ ///
+ ///
+ /// The returned URL can be absolute or relative. If possible, it gets converted based on .
+ ///
+ string? GetUrl();
+}
diff --git a/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs
index c085507365..7740141002 100644
--- a/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs
+++ b/src/JsonApiDotNetCore/Serialization/Response/LinkBuilder.cs
@@ -28,6 +28,8 @@ public class LinkBuilder : ILinkBuilder
private static readonly string GetRelationshipControllerActionName =
NoAsyncSuffix(nameof(BaseJsonApiController, int>.GetRelationshipAsync));
+ private static readonly UriNormalizer UriNormalizer = new();
+
private readonly IJsonApiOptions _options;
private readonly IJsonApiRequest _request;
private readonly IPaginationContext _paginationContext;
@@ -35,6 +37,7 @@ public class LinkBuilder : ILinkBuilder
private readonly LinkGenerator _linkGenerator;
private readonly IControllerResourceMapping _controllerResourceMapping;
private readonly IPaginationParser _paginationParser;
+ private readonly IDocumentDescriptionLinkProvider _documentDescriptionLinkProvider;
private HttpContext HttpContext
{
@@ -50,7 +53,8 @@ private HttpContext HttpContext
}
public LinkBuilder(IJsonApiOptions options, IJsonApiRequest request, IPaginationContext paginationContext, IHttpContextAccessor httpContextAccessor,
- LinkGenerator linkGenerator, IControllerResourceMapping controllerResourceMapping, IPaginationParser paginationParser)
+ LinkGenerator linkGenerator, IControllerResourceMapping controllerResourceMapping, IPaginationParser paginationParser,
+ IDocumentDescriptionLinkProvider documentDescriptionLinkProvider)
{
ArgumentGuard.NotNull(options);
ArgumentGuard.NotNull(request);
@@ -58,6 +62,7 @@ public LinkBuilder(IJsonApiOptions options, IJsonApiRequest request, IPagination
ArgumentGuard.NotNull(linkGenerator);
ArgumentGuard.NotNull(controllerResourceMapping);
ArgumentGuard.NotNull(paginationParser);
+ ArgumentGuard.NotNull(documentDescriptionLinkProvider);
_options = options;
_request = request;
@@ -66,6 +71,7 @@ public LinkBuilder(IJsonApiOptions options, IJsonApiRequest request, IPagination
_linkGenerator = linkGenerator;
_controllerResourceMapping = controllerResourceMapping;
_paginationParser = paginationParser;
+ _documentDescriptionLinkProvider = documentDescriptionLinkProvider;
}
private static string NoAsyncSuffix(string actionName)
@@ -94,6 +100,14 @@ private static string NoAsyncSuffix(string actionName)
SetPaginationInTopLevelLinks(resourceType!, links);
}
+ string? documentDescriptionUrl = _documentDescriptionLinkProvider.GetUrl();
+
+ if (!string.IsNullOrEmpty(documentDescriptionUrl))
+ {
+ var requestUri = new Uri(HttpContext.Request.GetEncodedUrl());
+ links.DescribedBy = UriNormalizer.Normalize(documentDescriptionUrl, _options.UseRelativeLinks, requestUri);
+ }
+
return links.HasValue() ? links : null;
}
diff --git a/src/JsonApiDotNetCore/Serialization/Response/NoDocumentDescriptionLinkProvider.cs b/src/JsonApiDotNetCore/Serialization/Response/NoDocumentDescriptionLinkProvider.cs
new file mode 100644
index 0000000000..c419e1ae35
--- /dev/null
+++ b/src/JsonApiDotNetCore/Serialization/Response/NoDocumentDescriptionLinkProvider.cs
@@ -0,0 +1,15 @@
+namespace JsonApiDotNetCore.Serialization.Response;
+
+///
+/// Provides no value for the "describedby" link in https://jsonapi.org/format/#document-top-level.
+///
+public sealed class NoDocumentDescriptionLinkProvider : IDocumentDescriptionLinkProvider
+{
+ ///
+ /// Always returns null .
+ ///
+ public string? GetUrl()
+ {
+ return null;
+ }
+}
diff --git a/src/JsonApiDotNetCore/Serialization/Response/UriNormalizer.cs b/src/JsonApiDotNetCore/Serialization/Response/UriNormalizer.cs
new file mode 100644
index 0000000000..5b2517f4b0
--- /dev/null
+++ b/src/JsonApiDotNetCore/Serialization/Response/UriNormalizer.cs
@@ -0,0 +1,80 @@
+namespace JsonApiDotNetCore.Serialization.Response;
+
+internal sealed class UriNormalizer
+{
+ ///
+ /// Converts a URL to absolute or relative format, if possible.
+ ///
+ ///
+ /// The absolute or relative URL to normalize.
+ ///
+ ///
+ /// Whether to convert to absolute or relative format.
+ ///
+ ///
+ /// The URL of the current HTTP request, whose path and query string are discarded.
+ ///
+ public string Normalize(string sourceUrl, bool preferRelative, Uri requestUri)
+ {
+ var sourceUri = new Uri(sourceUrl, UriKind.RelativeOrAbsolute);
+ Uri baseUri = RemovePathFromAbsoluteUri(requestUri);
+
+ if (!sourceUri.IsAbsoluteUri && !preferRelative)
+ {
+ var absoluteUri = new Uri(baseUri, sourceUrl);
+ return absoluteUri.AbsoluteUri;
+ }
+
+ if (sourceUri.IsAbsoluteUri && preferRelative)
+ {
+ if (AreSameServer(baseUri, sourceUri))
+ {
+ Uri relativeUri = baseUri.MakeRelativeUri(sourceUri);
+ return relativeUri.ToString();
+ }
+ }
+
+ return sourceUrl;
+ }
+
+ private static Uri RemovePathFromAbsoluteUri(Uri uri)
+ {
+ var requestUriBuilder = new UriBuilder(uri)
+ {
+ Path = null
+ };
+
+ return requestUriBuilder.Uri;
+ }
+
+ private static bool AreSameServer(Uri left, Uri right)
+ {
+ // Custom implementation because Uri.Equals() ignores the casing of username/password.
+
+ string leftScheme = left.GetComponents(UriComponents.Scheme, UriFormat.UriEscaped);
+ string rightScheme = right.GetComponents(UriComponents.Scheme, UriFormat.UriEscaped);
+
+ if (!string.Equals(leftScheme, rightScheme, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ string leftServer = left.GetComponents(UriComponents.HostAndPort, UriFormat.UriEscaped);
+ string rightServer = right.GetComponents(UriComponents.HostAndPort, UriFormat.UriEscaped);
+
+ if (!string.Equals(leftServer, rightServer, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ string leftUserInfo = left.GetComponents(UriComponents.UserInfo, UriFormat.UriEscaped);
+ string rightUserInfo = right.GetComponents(UriComponents.UserInfo, UriFormat.UriEscaped);
+
+ if (!string.Equals(leftUserInfo, rightUserInfo))
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs
index 3221215461..74e5aaa25b 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithNamespaceTests.cs
@@ -58,6 +58,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.ShouldNotBeNull();
@@ -101,6 +102,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.ManyValue.ShouldHaveCount(1);
@@ -167,6 +169,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
string albumLink = $"{HostPrefix}{PathPrefix}/photoAlbums/{photo.Album.StringId}";
@@ -211,6 +214,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.ManyValue.ShouldHaveCount(1);
@@ -259,6 +263,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.Should().BeNull();
@@ -293,6 +298,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.ManyValue.ShouldHaveCount(1);
responseDocument.Data.ManyValue[0].Links.Should().BeNull();
@@ -354,6 +360,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.ShouldNotBeNull();
@@ -437,6 +444,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
string photoLink = $"{HostPrefix}{PathPrefix}/photos/{existingPhoto.StringId}";
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs
index b6060e3d7c..6ba9636128 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/AbsoluteLinksWithoutNamespaceTests.cs
@@ -58,6 +58,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.ShouldNotBeNull();
@@ -101,6 +102,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.ManyValue.ShouldHaveCount(1);
@@ -167,6 +169,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
string albumLink = $"{HostPrefix}{PathPrefix}/photoAlbums/{photo.Album.StringId}";
@@ -211,6 +214,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.ManyValue.ShouldHaveCount(1);
@@ -259,6 +263,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.Should().BeNull();
@@ -293,6 +298,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.ManyValue.ShouldHaveCount(1);
responseDocument.Data.ManyValue[0].Links.Should().BeNull();
@@ -354,6 +360,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.ShouldNotBeNull();
@@ -437,6 +444,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
string photoLink = $"{HostPrefix}{PathPrefix}/photos/{existingPhoto.StringId}";
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/DocumentDescriptionLinkTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/DocumentDescriptionLinkTests.cs
new file mode 100644
index 0000000000..a2183247e5
--- /dev/null
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/DocumentDescriptionLinkTests.cs
@@ -0,0 +1,100 @@
+using System.Net;
+using FluentAssertions;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Serialization.Objects;
+using JsonApiDotNetCore.Serialization.Response;
+using Microsoft.Extensions.DependencyInjection;
+using TestBuildingBlocks;
+using Xunit;
+
+namespace JsonApiDotNetCoreTests.IntegrationTests.Links;
+
+public sealed class DocumentDescriptionLinkTests : IClassFixture, LinksDbContext>>
+{
+ private readonly IntegrationTestContext, LinksDbContext> _testContext;
+
+ public DocumentDescriptionLinkTests(IntegrationTestContext, LinksDbContext> testContext)
+ {
+ _testContext = testContext;
+
+ testContext.UseController();
+
+ testContext.ConfigureServices(services => services.AddSingleton());
+ }
+
+ [Fact]
+ public async Task Get_primary_resource_by_ID_converts_relative_documentation_link_to_absolute()
+ {
+ // Arrange
+ var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService();
+ options.UseRelativeLinks = false;
+
+ var provider = (TestDocumentDescriptionLinkProvider)_testContext.Factory.Services.GetRequiredService();
+ provider.Link = "description/json-schema?version=v1.0";
+
+ string route = $"/photos/{Unknown.StringId.For()}";
+
+ // Act
+ (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound);
+
+ responseDocument.Links.ShouldNotBeNull();
+ responseDocument.Links.DescribedBy.Should().Be("http://localhost/description/json-schema?version=v1.0");
+ }
+
+ [Fact]
+ public async Task Get_primary_resource_by_ID_converts_absolute_documentation_link_to_relative()
+ {
+ // Arrange
+ var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService();
+ options.UseRelativeLinks = true;
+
+ var provider = (TestDocumentDescriptionLinkProvider)_testContext.Factory.Services.GetRequiredService();
+ provider.Link = "http://localhost:80/description/json-schema?version=v1.0";
+
+ string route = $"/photos/{Unknown.StringId.For()}";
+
+ // Act
+ (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound);
+
+ responseDocument.Links.ShouldNotBeNull();
+ responseDocument.Links.DescribedBy.Should().Be("description/json-schema?version=v1.0");
+ }
+
+ [Fact]
+ public async Task Get_primary_resource_by_ID_cannot_convert_absolute_documentation_link_to_relative()
+ {
+ // Arrange
+ var options = (JsonApiOptions)_testContext.Factory.Services.GetRequiredService();
+ options.UseRelativeLinks = true;
+
+ var provider = (TestDocumentDescriptionLinkProvider)_testContext.Factory.Services.GetRequiredService();
+ provider.Link = "https://docs.api.com/description/json-schema?version=v1.0";
+
+ string route = $"/photos/{Unknown.StringId.For()}";
+
+ // Act
+ (HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecuteGetAsync(route);
+
+ // Assert
+ httpResponse.ShouldHaveStatusCode(HttpStatusCode.NotFound);
+
+ responseDocument.Links.ShouldNotBeNull();
+ responseDocument.Links.DescribedBy.Should().Be("https://docs.api.com/description/json-schema?version=v1.0");
+ }
+
+ private sealed class TestDocumentDescriptionLinkProvider : IDocumentDescriptionLinkProvider
+ {
+ public string? Link { get; set; }
+
+ public string? GetUrl()
+ {
+ return Link;
+ }
+ }
+}
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs
index 79a2b8408a..604f9c3e90 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithNamespaceTests.cs
@@ -58,6 +58,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.ShouldNotBeNull();
@@ -101,6 +102,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.ManyValue.ShouldHaveCount(1);
@@ -167,6 +169,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
string albumLink = $"{HostPrefix}{PathPrefix}/photoAlbums/{photo.Album.StringId}";
@@ -211,6 +214,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.ManyValue.ShouldHaveCount(1);
@@ -259,6 +263,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.Should().BeNull();
@@ -293,6 +298,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.ManyValue.ShouldHaveCount(1);
responseDocument.Data.ManyValue[0].Links.Should().BeNull();
@@ -354,6 +360,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.ShouldNotBeNull();
@@ -437,6 +444,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
string photoLink = $"{HostPrefix}{PathPrefix}/photos/{existingPhoto.StringId}";
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs
index 7e7e1d8f7a..6ce1effd7c 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/Links/RelativeLinksWithoutNamespaceTests.cs
@@ -58,6 +58,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.ShouldNotBeNull();
@@ -101,6 +102,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.ManyValue.ShouldHaveCount(1);
@@ -167,6 +169,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
string albumLink = $"{HostPrefix}{PathPrefix}/photoAlbums/{photo.Album.StringId}";
@@ -211,6 +214,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.ManyValue.ShouldHaveCount(1);
@@ -259,6 +263,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.Should().BeNull();
@@ -293,6 +298,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().Be(responseDocument.Links.Self);
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.ManyValue.ShouldHaveCount(1);
responseDocument.Data.ManyValue[0].Links.Should().BeNull();
@@ -354,6 +360,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
responseDocument.Data.SingleValue.ShouldNotBeNull();
responseDocument.Data.SingleValue.Links.ShouldNotBeNull();
@@ -437,6 +444,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
responseDocument.Links.Last.Should().BeNull();
responseDocument.Links.Prev.Should().BeNull();
responseDocument.Links.Next.Should().BeNull();
+ responseDocument.Links.DescribedBy.Should().BeNull();
string photoLink = $"{HostPrefix}{PathPrefix}/photos/{existingPhoto.StringId}";
diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs
index 392d61709a..94b9cb9386 100644
--- a/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs
+++ b/test/JsonApiDotNetCoreTests/UnitTests/Links/LinkInclusionTests.cs
@@ -88,7 +88,10 @@ public void Applies_cascading_settings_for_top_level_links(LinkTypes linksInReso
var linkGenerator = new FakeLinkGenerator();
var controllerResourceMapping = new FakeControllerResourceMapping();
var paginationParser = new PaginationParser();
- var linkBuilder = new LinkBuilder(options, request, paginationContext, httpContextAccessor, linkGenerator, controllerResourceMapping, paginationParser);
+ var documentDescriptionLinkProvider = new NoDocumentDescriptionLinkProvider();
+
+ var linkBuilder = new LinkBuilder(options, request, paginationContext, httpContextAccessor, linkGenerator, controllerResourceMapping, paginationParser,
+ documentDescriptionLinkProvider);
// Act
TopLevelLinks? topLevelLinks = linkBuilder.GetTopLevelLinks();
@@ -171,7 +174,10 @@ public void Applies_cascading_settings_for_resource_links(LinkTypes linksInResou
var linkGenerator = new FakeLinkGenerator();
var controllerResourceMapping = new FakeControllerResourceMapping();
var paginationParser = new PaginationParser();
- var linkBuilder = new LinkBuilder(options, request, paginationContext, httpContextAccessor, linkGenerator, controllerResourceMapping, paginationParser);
+ var documentDescriptionLinkProvider = new NoDocumentDescriptionLinkProvider();
+
+ var linkBuilder = new LinkBuilder(options, request, paginationContext, httpContextAccessor, linkGenerator, controllerResourceMapping, paginationParser,
+ documentDescriptionLinkProvider);
// Act
ResourceLinks? resourceLinks = linkBuilder.GetResourceLinks(exampleResourceType, new ExampleResource());
@@ -332,7 +338,10 @@ public void Applies_cascading_settings_for_relationship_links(LinkTypes linksInR
var linkGenerator = new FakeLinkGenerator();
var controllerResourceMapping = new FakeControllerResourceMapping();
var paginationParser = new PaginationParser();
- var linkBuilder = new LinkBuilder(options, request, paginationContext, httpContextAccessor, linkGenerator, controllerResourceMapping, paginationParser);
+ var documentDescriptionLinkProvider = new NoDocumentDescriptionLinkProvider();
+
+ var linkBuilder = new LinkBuilder(options, request, paginationContext, httpContextAccessor, linkGenerator, controllerResourceMapping, paginationParser,
+ documentDescriptionLinkProvider);
var relationship = new HasOneAttribute
{
diff --git a/test/JsonApiDotNetCoreTests/UnitTests/Links/UriNormalizerTests.cs b/test/JsonApiDotNetCoreTests/UnitTests/Links/UriNormalizerTests.cs
new file mode 100644
index 0000000000..545866c6ff
--- /dev/null
+++ b/test/JsonApiDotNetCoreTests/UnitTests/Links/UriNormalizerTests.cs
@@ -0,0 +1,78 @@
+using FluentAssertions;
+using JsonApiDotNetCore.Serialization.Response;
+using Xunit;
+
+namespace JsonApiDotNetCoreTests.UnitTests.Links;
+
+public sealed class UriNormalizerTests
+{
+ [Theory]
+ [InlineData("some/path", "http://localhost")]
+ [InlineData("some?version=1", "http://localhost")]
+ public void Keeps_relative_URL_relative(string sourceUrl, string requestUrl)
+ {
+ // Arrange
+ var normalizer = new UriNormalizer();
+
+ // Act
+ string result = normalizer.Normalize(sourceUrl, true, new Uri(requestUrl));
+
+ // Assert
+ result.Should().Be(sourceUrl);
+ }
+
+ [Theory]
+ [InlineData("some/path", "http://localhost", "http://localhost/some/path")]
+ [InlineData("some/path", "https://api-server.com", "https://api-server.com/some/path")]
+ [InlineData("some/path", "https://user:pass@api-server.com:9999", "https://user:pass@api-server.com:9999/some/path")]
+ [InlineData("some/path", "http://localhost/api/articles?debug=true#anchor", "http://localhost/some/path")]
+ [InlineData("some?version=1", "http://localhost/api/articles/1?debug=true#anchor", "http://localhost/some?version=1")]
+ public void Makes_relative_URL_absolute(string sourceUrl, string requestUrl, string expected)
+ {
+ // Arrange
+ var normalizer = new UriNormalizer();
+
+ // Act
+ string result = normalizer.Normalize(sourceUrl, false, new Uri(requestUrl));
+
+ // Assert
+ result.Should().Be(expected);
+ }
+
+ [Theory]
+ [InlineData("http://localhost/some/path", "http://api-server.com")]
+ [InlineData("http://localhost/some/path", "https://localhost")]
+ [InlineData("http://localhost:8080/some/path", "http://localhost")]
+ [InlineData("http://user:pass@localhost/some/path?version=1", "http://localhost")]
+ [InlineData("http://user:pass@localhost/some/path?version=1", "http://USER:PASS@localhost")]
+ public void Keeps_absolute_URL_absolute(string sourceUrl, string requestUrl)
+ {
+ // Arrange
+ var normalizer = new UriNormalizer();
+
+ // Act
+ string result = normalizer.Normalize(sourceUrl, true, new Uri(requestUrl));
+
+ // Assert
+ result.Should().Be(sourceUrl);
+ }
+
+ [Theory]
+ [InlineData("http://localhost/some/path", "http://localhost/api/articles/1", "some/path")]
+ [InlineData("http://api-server.com/some/path", "http://api-server.com/api/articles/1", "some/path")]
+ [InlineData("https://localhost/some/path", "https://localhost/api/articles/1", "some/path")]
+ [InlineData("https://localhost:443/some/path", "https://localhost/api/articles/1", "some/path")]
+ [InlineData("https://localhost/some/path", "https://localhost:443/api/articles/1", "some/path")]
+ [InlineData("HTTPS://LOCALHOST/some/path", "https://localhost:443/api/articles/1", "some/path")]
+ public void Makes_absolute_URL_relative(string sourceUrl, string requestUrl, string expected)
+ {
+ // Arrange
+ var normalizer = new UriNormalizer();
+
+ // Act
+ string result = normalizer.Normalize(sourceUrl, true, new Uri(requestUrl));
+
+ // Assert
+ result.Should().Be(expected);
+ }
+}
From e91885227749c6226432f1265ce44eebf229f1e7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 14 Mar 2024 02:27:53 +0000
Subject: [PATCH 23/63] Bump jetbrains.resharper.globaltools from 2023.3.3 to
2023.3.4 (#1498)
---
.config/dotnet-tools.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 240023b45a..2c16b97533 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"jetbrains.resharper.globaltools": {
- "version": "2023.3.3",
+ "version": "2023.3.4",
"commands": [
"jb"
]
From 9eaf9db52f085a2da11b7572ef64b07fd8b2965a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 15 Mar 2024 16:30:49 +0000
Subject: [PATCH 24/63] Bump dotnet-reportgenerator-globaltool from 5.2.2 to
5.2.3 (#1499)
---
.config/dotnet-tools.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 2c16b97533..12fd6de2b4 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -15,7 +15,7 @@
]
},
"dotnet-reportgenerator-globaltool": {
- "version": "5.2.2",
+ "version": "5.2.3",
"commands": [
"reportgenerator"
]
From a74a3eb836a0edf39a95c046831c4ccf6d5e15fa Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Sat, 16 Mar 2024 02:59:58 +0100
Subject: [PATCH 25/63] Fix some inconsistencies in tests (#1501)
---
.../ReadWrite/Deleting/DeleteResourceTests.cs | 4 ++--
.../IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs
index d489f9812d..9fb79db38f 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Deleting/DeleteResourceTests.cs
@@ -45,9 +45,9 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
await _testContext.RunOnDatabaseAsync(async dbContext =>
{
- WorkItem? workItemsInDatabase = await dbContext.WorkItems.FirstWithIdOrDefaultAsync(existingWorkItem.Id);
+ WorkItem? workItemInDatabase = await dbContext.WorkItems.FirstWithIdOrDefaultAsync(existingWorkItem.Id);
- workItemsInDatabase.Should().BeNull();
+ workItemInDatabase.Should().BeNull();
});
}
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs
index 853e09d95c..221ab16ec7 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/ReadWrite/Fetching/FetchResourceTests.cs
@@ -230,7 +230,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
}
[Fact]
- public async Task Can_get_unknown_secondary_OneToMany_resource()
+ public async Task Can_get_unknown_secondary_OneToMany_resources()
{
// Arrange
UserAccount userAccount = _fakers.UserAccount.Generate();
From 6fec2550fc3a9c726ed215bc575f596ffc60791d Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Sat, 16 Mar 2024 14:05:23 +0100
Subject: [PATCH 26/63] Remove unneeded [FromBody] from overridden controller
methods, add [Required] so OpenAPI can determine whether parameters are
required
---
.../Controllers/BaseJsonApiController.cs | 23 ++++++++++---------
.../BaseJsonApiOperationsController.cs | 4 +++-
.../Controllers/JsonApiController.cs | 8 +++----
.../JsonApiOperationsController.cs | 2 +-
.../ObfuscatedIdentifiableController.cs | 11 +++++----
5 files changed, 26 insertions(+), 22 deletions(-)
diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs
index fb3cd2bd2d..1d518c6194 100644
--- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs
+++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs
@@ -1,3 +1,4 @@
+using System.ComponentModel.DataAnnotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Middleware;
@@ -106,7 +107,7 @@ public virtual async Task GetAsync(CancellationToken cancellation
/// GET /articles/1 HTTP/1.1
/// ]]>
///
- public virtual async Task GetAsync(TId id, CancellationToken cancellationToken)
+ public virtual async Task GetAsync([Required] TId id, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -131,7 +132,7 @@ public virtual async Task GetAsync(TId id, CancellationToken canc
/// GET /articles/1/revisions HTTP/1.1
/// ]]>
///
- public virtual async Task GetSecondaryAsync(TId id, string relationshipName, CancellationToken cancellationToken)
+ public virtual async Task GetSecondaryAsync([Required] TId id, [Required] string relationshipName, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -160,7 +161,7 @@ public virtual async Task GetSecondaryAsync(TId id, string relati
/// GET /articles/1/relationships/revisions HTTP/1.1
/// ]]>
///
- public virtual async Task GetRelationshipAsync(TId id, string relationshipName, CancellationToken cancellationToken)
+ public virtual async Task GetRelationshipAsync([Required] TId id, [Required] string relationshipName, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -185,7 +186,7 @@ public virtual async Task GetRelationshipAsync(TId id, string rel
/// POST /articles HTTP/1.1
/// ]]>
///
- public virtual async Task PostAsync([FromBody] TResource resource, CancellationToken cancellationToken)
+ public virtual async Task PostAsync([FromBody] [Required] TResource resource, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -235,8 +236,8 @@ public virtual async Task PostAsync([FromBody] TResource resource
///
/// Propagates notification that request handling should be canceled.
///
- public virtual async Task PostRelationshipAsync(TId id, string relationshipName, [FromBody] ISet rightResourceIds,
- CancellationToken cancellationToken)
+ public virtual async Task PostRelationshipAsync([Required] TId id, [Required] string relationshipName,
+ [FromBody] [Required] ISet rightResourceIds, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -264,7 +265,7 @@ public virtual async Task PostRelationshipAsync(TId id, string re
/// PATCH /articles/1 HTTP/1.1
/// ]]>
///
- public virtual async Task PatchAsync(TId id, [FromBody] TResource resource, CancellationToken cancellationToken)
+ public virtual async Task PatchAsync([Required] TId id, [FromBody] [Required] TResource resource, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -310,7 +311,7 @@ public virtual async Task PatchAsync(TId id, [FromBody] TResource
///
/// Propagates notification that request handling should be canceled.
///
- public virtual async Task PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object? rightValue,
+ public virtual async Task PatchRelationshipAsync([Required] TId id, [Required] string relationshipName, [FromBody] object? rightValue,
CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
@@ -337,7 +338,7 @@ public virtual async Task PatchRelationshipAsync(TId id, string r
/// DELETE /articles/1 HTTP/1.1
/// ]]>
///
- public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToken)
+ public virtual async Task DeleteAsync([Required] TId id, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -371,8 +372,8 @@ public virtual async Task DeleteAsync(TId id, CancellationToken c
///
/// Propagates notification that request handling should be canceled.
///
- public virtual async Task DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet rightResourceIds,
- CancellationToken cancellationToken)
+ public virtual async Task DeleteRelationshipAsync([Required] TId id, [Required] string relationshipName,
+ [FromBody] [Required] ISet rightResourceIds, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
index 596b22794d..f16a2a6686 100644
--- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
+++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
@@ -1,3 +1,4 @@
+using System.ComponentModel.DataAnnotations;
using JetBrains.Annotations;
using JsonApiDotNetCore.AtomicOperations;
using JsonApiDotNetCore.Configuration;
@@ -102,7 +103,8 @@ protected BaseJsonApiOperationsController(IJsonApiOptions options, IResourceGrap
/// }
/// ]]>
///
- public virtual async Task PostOperationsAsync([FromBody] IList operations, CancellationToken cancellationToken)
+ public virtual async Task PostOperationsAsync([FromBody] [Required] IList operations,
+ CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs
index 19c679404f..82719bfeac 100644
--- a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs
+++ b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs
@@ -72,14 +72,14 @@ public override Task GetRelationshipAsync(TId id, string relation
///
[HttpPost]
- public override Task PostAsync([FromBody] TResource resource, CancellationToken cancellationToken)
+ public override Task PostAsync(TResource resource, CancellationToken cancellationToken)
{
return base.PostAsync(resource, cancellationToken);
}
///
[HttpPost("{id}/relationships/{relationshipName}")]
- public override Task PostRelationshipAsync(TId id, string relationshipName, [FromBody] ISet rightResourceIds,
+ public override Task PostRelationshipAsync(TId id, string relationshipName, ISet rightResourceIds,
CancellationToken cancellationToken)
{
return base.PostRelationshipAsync(id, relationshipName, rightResourceIds, cancellationToken);
@@ -87,7 +87,7 @@ public override Task PostRelationshipAsync(TId id, string relatio
///
[HttpPatch("{id}")]
- public override Task PatchAsync(TId id, [FromBody] TResource resource, CancellationToken cancellationToken)
+ public override Task PatchAsync(TId id, TResource resource, CancellationToken cancellationToken)
{
return base.PatchAsync(id, resource, cancellationToken);
}
@@ -109,7 +109,7 @@ public override Task DeleteAsync(TId id, CancellationToken cancel
///
[HttpDelete("{id}/relationships/{relationshipName}")]
- public override Task DeleteRelationshipAsync(TId id, string relationshipName, [FromBody] ISet rightResourceIds,
+ public override Task DeleteRelationshipAsync(TId id, string relationshipName, ISet rightResourceIds,
CancellationToken cancellationToken)
{
return base.DeleteRelationshipAsync(id, relationshipName, rightResourceIds, cancellationToken);
diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs
index 43f75896a0..bacb5b9342 100644
--- a/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs
+++ b/src/JsonApiDotNetCore/Controllers/JsonApiOperationsController.cs
@@ -17,7 +17,7 @@ public abstract class JsonApiOperationsController(
{
///
[HttpPost]
- public override Task PostOperationsAsync([FromBody] IList operations, CancellationToken cancellationToken)
+ public override Task PostOperationsAsync(IList operations, CancellationToken cancellationToken)
{
return base.PostOperationsAsync(operations, cancellationToken);
}
diff --git a/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs b/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs
index 4f8c5af49b..46391aab7c 100644
--- a/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs
+++ b/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation/ObfuscatedIdentifiableController.cs
@@ -1,3 +1,4 @@
+using System.ComponentModel.DataAnnotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Controllers;
using JsonApiDotNetCore.Resources;
@@ -42,13 +43,13 @@ public Task GetRelationshipAsync(string id, string relationshipNa
}
[HttpPost]
- public override Task PostAsync([FromBody] TResource resource, CancellationToken cancellationToken)
+ public override Task PostAsync([FromBody] [Required] TResource resource, CancellationToken cancellationToken)
{
return base.PostAsync(resource, cancellationToken);
}
[HttpPost("{id}/relationships/{relationshipName}")]
- public Task PostRelationshipAsync(string id, string relationshipName, [FromBody] ISet rightResourceIds,
+ public Task PostRelationshipAsync(string id, string relationshipName, [FromBody] [Required] ISet rightResourceIds,
CancellationToken cancellationToken)
{
int idValue = _codec.Decode(id);
@@ -56,14 +57,14 @@ public Task PostRelationshipAsync(string id, string relationshipN
}
[HttpPatch("{id}")]
- public Task PatchAsync(string id, [FromBody] TResource resource, CancellationToken cancellationToken)
+ public Task PatchAsync(string id, [FromBody] [Required] TResource resource, CancellationToken cancellationToken)
{
int idValue = _codec.Decode(id);
return base.PatchAsync(idValue, resource, cancellationToken);
}
[HttpPatch("{id}/relationships/{relationshipName}")]
- public Task PatchRelationshipAsync(string id, string relationshipName, [FromBody] object rightValue, CancellationToken cancellationToken)
+ public Task PatchRelationshipAsync(string id, string relationshipName, [FromBody] object? rightValue, CancellationToken cancellationToken)
{
int idValue = _codec.Decode(id);
return base.PatchRelationshipAsync(idValue, relationshipName, rightValue, cancellationToken);
@@ -77,7 +78,7 @@ public Task DeleteAsync(string id, CancellationToken cancellation
}
[HttpDelete("{id}/relationships/{relationshipName}")]
- public Task DeleteRelationshipAsync(string id, string relationshipName, [FromBody] ISet rightResourceIds,
+ public Task DeleteRelationshipAsync(string id, string relationshipName, [FromBody] [Required] ISet rightResourceIds,
CancellationToken cancellationToken)
{
int idValue = _codec.Decode(id);
From 6bf4a8cd238bfcefb2784bdb720863386c571f5a Mon Sep 17 00:00:00 2001
From: Bart Koelman <10324372+bkoelman@users.noreply.github.com>
Date: Sun, 17 Mar 2024 00:40:10 +0100
Subject: [PATCH 27/63] Move back [FromBody] and [Required] to derived
controllers, reverting most of the previous PR (#1503). It turns out that
ASP.NET ModelState doesn't look at attributes on base parameters
---
.../Controllers/BaseJsonApiController.cs | 24 +++++++++----------
.../BaseJsonApiOperationsController.cs | 4 +---
.../Controllers/JsonApiController.cs | 24 ++++++++++---------
.../JsonApiOperationsController.cs | 3 ++-
.../ObfuscatedIdentifiableController.cs | 22 +++++++++--------
5 files changed, 39 insertions(+), 38 deletions(-)
diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs
index 1d518c6194..5fee532b36 100644
--- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs
+++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs
@@ -1,4 +1,3 @@
-using System.ComponentModel.DataAnnotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Middleware;
@@ -107,7 +106,7 @@ public virtual async Task GetAsync(CancellationToken cancellation
/// GET /articles/1 HTTP/1.1
/// ]]>
///
- public virtual async Task GetAsync([Required] TId id, CancellationToken cancellationToken)
+ public virtual async Task GetAsync(TId id, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -132,7 +131,7 @@ public virtual async Task GetAsync([Required] TId id, Cancellatio
/// GET /articles/1/revisions HTTP/1.1
/// ]]>
///
- public virtual async Task GetSecondaryAsync([Required] TId id, [Required] string relationshipName, CancellationToken cancellationToken)
+ public virtual async Task GetSecondaryAsync(TId id, string relationshipName, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -161,7 +160,7 @@ public virtual async Task GetSecondaryAsync([Required] TId id, [R
/// GET /articles/1/relationships/revisions HTTP/1.1
/// ]]>
///
- public virtual async Task GetRelationshipAsync([Required] TId id, [Required] string relationshipName, CancellationToken cancellationToken)
+ public virtual async Task GetRelationshipAsync(TId id, string relationshipName, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -186,7 +185,7 @@ public virtual async Task GetRelationshipAsync([Required] TId id,
/// POST /articles HTTP/1.1
/// ]]>
///
- public virtual async Task PostAsync([FromBody] [Required] TResource resource, CancellationToken cancellationToken)
+ public virtual async Task PostAsync(TResource resource, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -236,8 +235,8 @@ public virtual async Task PostAsync([FromBody] [Required] TResour
///
/// Propagates notification that request handling should be canceled.
///
- public virtual async Task PostRelationshipAsync([Required] TId id, [Required] string relationshipName,
- [FromBody] [Required] ISet rightResourceIds, CancellationToken cancellationToken)
+ public virtual async Task PostRelationshipAsync(TId id, string relationshipName, ISet rightResourceIds,
+ CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -265,7 +264,7 @@ public virtual async Task PostRelationshipAsync([Required] TId id
/// PATCH /articles/1 HTTP/1.1
/// ]]>
///
- public virtual async Task PatchAsync([Required] TId id, [FromBody] [Required] TResource resource, CancellationToken cancellationToken)
+ public virtual async Task PatchAsync(TId id, TResource resource, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -311,8 +310,7 @@ public virtual async Task PatchAsync([Required] TId id, [FromBody
///
/// Propagates notification that request handling should be canceled.
///
- public virtual async Task PatchRelationshipAsync([Required] TId id, [Required] string relationshipName, [FromBody] object? rightValue,
- CancellationToken cancellationToken)
+ public virtual async Task PatchRelationshipAsync(TId id, string relationshipName, object? rightValue, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -338,7 +336,7 @@ public virtual async Task PatchRelationshipAsync([Required] TId i
/// DELETE /articles/1 HTTP/1.1
/// ]]>
///
- public virtual async Task DeleteAsync([Required] TId id, CancellationToken cancellationToken)
+ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
@@ -372,8 +370,8 @@ public virtual async Task DeleteAsync([Required] TId id, Cancella
///
/// Propagates notification that request handling should be canceled.
///
- public virtual async Task DeleteRelationshipAsync([Required] TId id, [Required] string relationshipName,
- [FromBody] [Required] ISet rightResourceIds, CancellationToken cancellationToken)
+ public virtual async Task DeleteRelationshipAsync(TId id, string relationshipName, ISet rightResourceIds,
+ CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
index f16a2a6686..1e7da80bd1 100644
--- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
+++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs
@@ -1,4 +1,3 @@
-using System.ComponentModel.DataAnnotations;
using JetBrains.Annotations;
using JsonApiDotNetCore.AtomicOperations;
using JsonApiDotNetCore.Configuration;
@@ -103,8 +102,7 @@ protected BaseJsonApiOperationsController(IJsonApiOptions options, IResourceGrap
/// }
/// ]]>
///
- public virtual async Task PostOperationsAsync([FromBody] [Required] IList operations,
- CancellationToken cancellationToken)
+ public virtual async Task PostOperationsAsync(IList operations, CancellationToken cancellationToken)
{
_traceWriter.LogMethodStart(new
{
diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs
index 82719bfeac..0c56500797 100644
--- a/src/JsonApiDotNetCore/Controllers/JsonApiController.cs
+++ b/src/JsonApiDotNetCore/Controllers/JsonApiController.cs
@@ -1,3 +1,4 @@
+using System.ComponentModel.DataAnnotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Services;
@@ -49,7 +50,7 @@ public override Task GetAsync(CancellationToken cancellationToken
///
[HttpGet("{id}")]
[HttpHead("{id}")]
- public override Task GetAsync(TId id, CancellationToken cancellationToken)
+ public override Task GetAsync([Required] TId id, CancellationToken cancellationToken)
{
return base.GetAsync(id, cancellationToken);
}
@@ -57,7 +58,7 @@ public override Task GetAsync(TId id, CancellationToken cancellat
///
[HttpGet("{id}/{relationshipName}")]
[HttpHead("{id}/{relationshipName}")]
- public override Task GetSecondaryAsync(TId id, string relationshipName, CancellationToken cancellationToken)
+ public override Task GetSecondaryAsync([Required] TId id, [Required] string relationshipName, CancellationToken cancellationToken)
{
return base.GetSecondaryAsync(id, relationshipName, cancellationToken);
}
@@ -65,36 +66,37 @@ public override Task GetSecondaryAsync(TId id, string relationshi
///
[HttpGet("{id}/relationships/{relationshipName}")]
[HttpHead("{id}/relationships/{relationshipName}")]
- public override Task GetRelationshipAsync(TId id, string relationshipName, CancellationToken cancellationToken)
+ public override Task GetRelationshipAsync([Required] TId id, [Required] string relationshipName, CancellationToken cancellationToken)
{
return base.GetRelationshipAsync(id, relationshipName, cancellationToken);
}
///
[HttpPost]
- public override Task PostAsync(TResource resource, CancellationToken cancellationToken)
+ public override Task PostAsync([FromBody] [Required] TResource resource, CancellationToken cancellationToken)
{
return base.PostAsync(resource, cancellationToken);
}
///
[HttpPost("{id}/relationships/{relationshipName}")]
- public override Task PostRelationshipAsync(TId id, string relationshipName, ISet rightResourceIds,
- CancellationToken cancellationToken)
+ public override Task PostRelationshipAsync([Required] TId id, [Required] string relationshipName,
+ [FromBody] [Required] ISet rightResourceIds, CancellationToken cancellationToken)
{
return base.PostRelationshipAsync(id, relationshipName, rightResourceIds, cancellationToken);
}
///
[HttpPatch("{id}")]
- public override Task PatchAsync(TId id, TResource resource, CancellationToken cancellationToken)
+ public override Task PatchAsync([Required] TId id, [FromBody] [Required] TResource resource, CancellationToken cancellationToken)
{
return base.PatchAsync(id, resource, cancellationToken);
}
///
[HttpPatch("{id}/relationships/{relationshipName}")]
- public override Task PatchRelationshipAsync(TId id, string relationshipName, [FromBody] object? rightValue,
+ // Parameter `[Required] object? rightValue` makes Swashbuckle generate the OpenAPI request body as required. We don't actually validate ModelState, so it doesn't hurt.
+ public override Task PatchRelationshipAsync([Required] TId id, [Required] string relationshipName, [FromBody] [Required] object? rightValue,
CancellationToken cancellationToken)
{
return base.PatchRelationshipAsync(id, relationshipName, rightValue, cancellationToken);
@@ -102,15 +104,15 @@ public override Task PatchRelationshipAsync(TId id, string relati
///
[HttpDelete("{id}")]
- public override Task DeleteAsync(TId id, CancellationToken cancellationToken)
+ public override Task