Skip to content

Extensible query string functions #1286

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 16 commits into from
Jul 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions benchmarks/QueryString/QueryStringParserBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
using JsonApiDotNetCore;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries.Parsing;
using JsonApiDotNetCore.QueryStrings;
using JsonApiDotNetCore.QueryStrings.Internal;
using JsonApiDotNetCore.Resources;
using Microsoft.Extensions.Logging.Abstractions;

Expand Down Expand Up @@ -37,11 +37,23 @@ public QueryStringParserBenchmarks()

var resourceFactory = new ResourceFactory(new ServiceContainer());

var includeReader = new IncludeQueryStringParameterReader(request, resourceGraph, options);
var filterReader = new FilterQueryStringParameterReader(request, resourceGraph, resourceFactory, options);
var sortReader = new SortQueryStringParameterReader(request, resourceGraph);
var sparseFieldSetReader = new SparseFieldSetQueryStringParameterReader(request, resourceGraph);
var paginationReader = new PaginationQueryStringParameterReader(request, resourceGraph, options);
var includeParser = new IncludeParser(options);
var includeReader = new IncludeQueryStringParameterReader(includeParser, request, resourceGraph);

var filterScopeParser = new QueryStringParameterScopeParser();
var filterValueParser = new FilterParser(resourceFactory);
var filterReader = new FilterQueryStringParameterReader(filterScopeParser, filterValueParser, request, resourceGraph, options);

var sortScopeParser = new QueryStringParameterScopeParser();
var sortValueParser = new SortParser();
var sortReader = new SortQueryStringParameterReader(sortScopeParser, sortValueParser, request, resourceGraph);

var sparseFieldSetScopeParser = new SparseFieldTypeParser(resourceGraph);
var sparseFieldSetValueParser = new SparseFieldSetParser();
var sparseFieldSetReader = new SparseFieldSetQueryStringParameterReader(sparseFieldSetScopeParser, sparseFieldSetValueParser, request, resourceGraph);

var paginationParser = new PaginationParser();
var paginationReader = new PaginationQueryStringParameterReader(paginationParser, request, resourceGraph, options);

IQueryStringParameterReader[] readers = ArrayFactory.Create<IQueryStringParameterReader>(includeReader, filterReader, sortReader,
sparseFieldSetReader, paginationReader);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Queries.Internal;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Serialization.Objects;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Queries.Expressions;
using JsonApiDotNetCore.Queries.Internal;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Serialization.Objects;

Expand Down
1 change: 0 additions & 1 deletion benchmarks/Serialization/SerializationBenchmarkBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Queries.Internal;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Serialization.Response;
Expand Down
2 changes: 2 additions & 0 deletions benchmarks/Tools/NeverResourceDefinitionAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries.Expressions;
using JsonApiDotNetCore.Queries.QueryableBuilding;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;

Expand All @@ -13,6 +14,7 @@ namespace Benchmarks.Tools;
internal sealed class NeverResourceDefinitionAccessor : IResourceDefinitionAccessor
{
bool IResourceDefinitionAccessor.IsReadOnlyRequest => throw new NotImplementedException();
public IQueryableBuilder QueryableBuilder => throw new NotImplementedException();

public IImmutableSet<IncludeElementExpression> OnApplyIncludes(ResourceType resourceType, IImmutableSet<IncludeElementExpression> existingIncludes)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
using System.Collections;
using System.Linq.Expressions;
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Queries.Internal.QueryableBuilding;
using JsonApiDotNetCore.Queries.QueryableBuilding;
using JsonApiDotNetCore.Resources;
using Microsoft.EntityFrameworkCore.Metadata;

namespace NoEntityFrameworkExample;

internal sealed class QueryLayerToLinqConverter
{
private readonly IResourceFactory _resourceFactory;
private readonly IModel _model;
private readonly IQueryableBuilder _queryableBuilder;

public QueryLayerToLinqConverter(IResourceFactory resourceFactory, IModel model)
public QueryLayerToLinqConverter(IModel model, IQueryableBuilder queryableBuilder)
{
_resourceFactory = resourceFactory;
_model = model;
_queryableBuilder = queryableBuilder;
}

public IEnumerable<TResource> ApplyQueryLayer<TResource>(QueryLayer queryLayer, IEnumerable<TResource> resources)
Expand All @@ -26,10 +26,9 @@ public IEnumerable<TResource> ApplyQueryLayer<TResource>(QueryLayer queryLayer,
converter.ConvertIncludesToSelections();

// Convert QueryLayer into LINQ expression.
Expression source = ((IEnumerable)resources).AsQueryable().Expression;
var nameFactory = new LambdaParameterNameFactory();
var queryableBuilder = new QueryableBuilder(source, queryLayer.ResourceType.ClrType, typeof(Enumerable), nameFactory, _resourceFactory, _model);
Expression expression = queryableBuilder.ApplyQuery(queryLayer);
IQueryable source = ((IEnumerable)resources).AsQueryable();
var context = QueryableBuilderContext.CreateRoot(source, typeof(Enumerable), _model, null);
Expression expression = _queryableBuilder.ApplyQuery(queryLayer, context);

// Insert null checks to prevent a NullReferenceException during execution of expressions such as:
// 'todoItems => todoItems.Where(todoItem => todoItem.Assignee.Id == 1)' when a TodoItem doesn't have an assignee.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Queries.Expressions;
using JsonApiDotNetCore.Queries.Internal.QueryableBuilding;
using JsonApiDotNetCore.Queries.QueryableBuilding;
using JsonApiDotNetCore.Repositories;
using JsonApiDotNetCore.Resources;
using NoEntityFrameworkExample.Data;
Expand All @@ -25,12 +25,12 @@ public abstract class InMemoryResourceRepository<TResource, TId> : IResourceRead
private readonly ResourceType _resourceType;
private readonly QueryLayerToLinqConverter _queryLayerToLinqConverter;

protected InMemoryResourceRepository(IResourceGraph resourceGraph, IResourceFactory resourceFactory)
protected InMemoryResourceRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder)
{
_resourceType = resourceGraph.GetResourceType<TResource>();

var model = new InMemoryModel(resourceGraph);
_queryLayerToLinqConverter = new QueryLayerToLinqConverter(resourceFactory, model);
_queryLayerToLinqConverter = new QueryLayerToLinqConverter(model, queryableBuilder);
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Queries.QueryableBuilding;
using NoEntityFrameworkExample.Data;
using NoEntityFrameworkExample.Models;

Expand All @@ -9,8 +9,8 @@ namespace NoEntityFrameworkExample.Repositories;
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
public sealed class PersonRepository : InMemoryResourceRepository<Person, long>
{
public PersonRepository(IResourceGraph resourceGraph, IResourceFactory resourceFactory)
: base(resourceGraph, resourceFactory)
public PersonRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder)
: base(resourceGraph, queryableBuilder)
{
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Queries.QueryableBuilding;
using NoEntityFrameworkExample.Data;
using NoEntityFrameworkExample.Models;

Expand All @@ -9,8 +9,8 @@ namespace NoEntityFrameworkExample.Repositories;
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
public sealed class TagRepository : InMemoryResourceRepository<Tag, long>
{
public TagRepository(IResourceGraph resourceGraph, IResourceFactory resourceFactory)
: base(resourceGraph, resourceFactory)
public TagRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder)
: base(resourceGraph, queryableBuilder)
{
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Queries.QueryableBuilding;
using NoEntityFrameworkExample.Data;
using NoEntityFrameworkExample.Models;

Expand All @@ -9,8 +9,8 @@ namespace NoEntityFrameworkExample.Repositories;
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
public sealed class TodoItemRepository : InMemoryResourceRepository<TodoItem, long>
{
public TodoItemRepository(IResourceGraph resourceGraph, IResourceFactory resourceFactory)
: base(resourceGraph, resourceFactory)
public TodoItemRepository(IResourceGraph resourceGraph, IQueryableBuilder queryableBuilder)
: base(resourceGraph, queryableBuilder)
{
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Queries.Expressions;
using JsonApiDotNetCore.Queries.Internal.QueryableBuilding;
using JsonApiDotNetCore.Queries.QueryableBuilding;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Services;
Expand Down Expand Up @@ -42,7 +42,7 @@ public abstract class InMemoryResourceService<TResource, TId> : IResourceQuerySe
private readonly QueryLayerToLinqConverter _queryLayerToLinqConverter;

protected InMemoryResourceService(IJsonApiOptions options, IResourceGraph resourceGraph, IQueryLayerComposer queryLayerComposer,
IResourceFactory resourceFactory, IPaginationContext paginationContext, IEnumerable<IQueryConstraintProvider> constraintProviders,
IPaginationContext paginationContext, IEnumerable<IQueryConstraintProvider> constraintProviders, IQueryableBuilder queryableBuilder,
ILoggerFactory loggerFactory)
{
_options = options;
Expand All @@ -54,7 +54,7 @@ protected InMemoryResourceService(IJsonApiOptions options, IResourceGraph resour
_resourceType = resourceGraph.GetResourceType<TResource>();

var model = new InMemoryModel(resourceGraph);
_queryLayerToLinqConverter = new QueryLayerToLinqConverter(resourceFactory, model);
_queryLayerToLinqConverter = new QueryLayerToLinqConverter(model, queryableBuilder);
}

/// <inheritdoc />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Queries.QueryableBuilding;
using JsonApiDotNetCore.Resources;
using NoEntityFrameworkExample.Data;
using NoEntityFrameworkExample.Models;
Expand All @@ -10,9 +11,9 @@ namespace NoEntityFrameworkExample.Services;
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
public sealed class TodoItemService : InMemoryResourceService<TodoItem, long>
{
public TodoItemService(IJsonApiOptions options, IResourceGraph resourceGraph, IQueryLayerComposer queryLayerComposer, IResourceFactory resourceFactory,
IPaginationContext paginationContext, IEnumerable<IQueryConstraintProvider> constraintProviders, ILoggerFactory loggerFactory)
: base(options, resourceGraph, queryLayerComposer, resourceFactory, paginationContext, constraintProviders, loggerFactory)
public TodoItemService(IJsonApiOptions options, IResourceGraph resourceGraph, IQueryLayerComposer queryLayerComposer, IPaginationContext paginationContext,
IEnumerable<IQueryConstraintProvider> constraintProviders, IQueryableBuilder queryableBuilder, ILoggerFactory loggerFactory)
: base(options, resourceGraph, queryLayerComposer, paginationContext, constraintProviders, queryableBuilder, loggerFactory)
{
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations.Schema;
using JsonApiDotNetCore.Resources.Internal;

namespace JsonApiDotNetCore.Resources;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,31 @@

#pragma warning disable AV1008 // Class should not be static

namespace JsonApiDotNetCore.Resources.Internal;
namespace JsonApiDotNetCore.Resources;

/// <summary>
/// Provides utilities regarding runtime types.
/// </summary>
[PublicAPI]
public static class RuntimeTypeConverter
{
private const string ParseQueryStringsUsingCurrentCultureSwitchName = "JsonApiDotNetCore.ParseQueryStringsUsingCurrentCulture";

/// <summary>
/// Converts the specified value to the specified type.
/// </summary>
/// <param name="value">
/// The value to convert from.
/// </param>
/// <param name="type">
/// The type to convert to.
/// </param>
/// <returns>
/// The converted type, or <c>null</c> if <paramref name="value" /> is <c>null</c> and <paramref name="type" /> is a nullable type.
/// </returns>
/// <exception cref="FormatException">
/// <paramref name="value" /> is not compatible with <paramref name="type" />.
/// </exception>
public static object? ConvertType(object? value, Type type)
{
ArgumentGuard.NotNull(type);
Expand Down Expand Up @@ -114,11 +132,20 @@ public static class RuntimeTypeConverter
}
}

/// <summary>
/// Indicates whether the specified type is a nullable value type or a reference type.
/// </summary>
public static bool CanContainNull(Type type)
{
return !type.IsValueType || Nullable.GetUnderlyingType(type) != null;
}

/// <summary>
/// Gets the default value for the specified type.
/// </summary>
/// <returns>
/// The default value, or <c>null</c> for nullable value types and reference types.
/// </returns>
public static object? GetDefaultValue(Type type)
{
return type.IsValueType ? Activator.CreateInstance(type) : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries.Internal;
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Serialization.Objects;

Expand Down
12 changes: 12 additions & 0 deletions src/JsonApiDotNetCore/CollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ public static int FindIndex<T>(this IReadOnlyList<T> source, Predicate<T> match)
return -1;
}

public static IEnumerable<T> ToEnumerable<T>(this LinkedListNode<T>? startNode)
{
LinkedListNode<T>? current = startNode;

while (current != null)
{
yield return current.Value;

current = current.Next;
}
}

public static bool DictionaryEqual<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue>? first, IReadOnlyDictionary<TKey, TValue>? second,
IEqualityComparer<TValue>? valueComparer = null)
{
Expand Down
19 changes: 17 additions & 2 deletions src/JsonApiDotNetCore/Configuration/JsonApiApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
using JsonApiDotNetCore.AtomicOperations.Processors;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.Queries.Internal;
using JsonApiDotNetCore.Queries.Parsing;
using JsonApiDotNetCore.Queries.QueryableBuilding;
using JsonApiDotNetCore.QueryStrings;
using JsonApiDotNetCore.QueryStrings.Internal;
using JsonApiDotNetCore.Repositories;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Serialization.JsonConverters;
Expand Down Expand Up @@ -193,6 +193,13 @@ private void AddRepositoryLayer()
RegisterImplementationForInterfaces(ServiceDiscoveryFacade.RepositoryUnboundInterfaces, typeof(EntityFrameworkCoreRepository<,>));

_services.AddScoped<IResourceRepositoryAccessor, ResourceRepositoryAccessor>();

_services.TryAddTransient<IQueryableBuilder, QueryableBuilder>();
_services.TryAddTransient<IIncludeClauseBuilder, IncludeClauseBuilder>();
_services.TryAddTransient<IOrderClauseBuilder, OrderClauseBuilder>();
_services.TryAddTransient<ISelectClauseBuilder, SelectClauseBuilder>();
_services.TryAddTransient<ISkipTakeClauseBuilder, SkipTakeClauseBuilder>();
_services.TryAddTransient<IWhereClauseBuilder, WhereClauseBuilder>();
}

private void AddServiceLayer()
Expand All @@ -210,6 +217,14 @@ private void RegisterImplementationForInterfaces(HashSet<Type> unboundInterfaces

private void AddQueryStringLayer()
{
_services.TryAddTransient<IQueryStringParameterScopeParser, QueryStringParameterScopeParser>();
_services.TryAddTransient<IIncludeParser, IncludeParser>();
_services.TryAddTransient<IFilterParser, FilterParser>();
_services.TryAddTransient<ISortParser, SortParser>();
_services.TryAddTransient<ISparseFieldTypeParser, SparseFieldTypeParser>();
_services.TryAddTransient<ISparseFieldSetParser, SparseFieldSetParser>();
_services.TryAddTransient<IPaginationParser, PaginationParser>();

_services.AddScoped<IIncludeQueryStringParameterReader, IncludeQueryStringParameterReader>();
_services.AddScoped<IFilterQueryStringParameterReader, FilterQueryStringParameterReader>();
_services.AddScoped<ISortQueryStringParameterReader, SortQueryStringParameterReader>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using JsonApiDotNetCore.Queries.Expressions;

namespace JsonApiDotNetCore.Queries.Internal;
namespace JsonApiDotNetCore.Queries;

/// <inheritdoc />
internal sealed class EvaluatedIncludeCache : IEvaluatedIncludeCache
Expand Down
Loading