Skip to content

Query parser service #190

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 2 commits into from
Nov 13, 2017
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
12 changes: 2 additions & 10 deletions src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,8 @@ public class JsonApiOptions
public bool AllowClientGeneratedIds { get; set; }
public IContextGraph ContextGraph { get; set; }
public bool RelativeLinks { get; set; }

/// <summary>
/// This flag is experimental and could be perceived as a violation
/// of the v1 spec. However, we have decided that this is a real
/// requirement for users of this library and a gap in the specification.
/// It will likely be removed when the spec is updated to support this
/// requirement.
/// </summary>
public bool AllowCustomQueryParameters { get; set; }

[Obsolete("JsonContract resolver can now be set on SerializerSettings.")]
public IContractResolver JsonContractResolver
{
Expand All @@ -38,8 +31,7 @@ public IContractResolver JsonContractResolver
};
internal IContextGraphBuilder ContextGraphBuilder { get; } = new ContextGraphBuilder();

public void BuildContextGraph<TContext>(Action<IContextGraphBuilder> builder)
where TContext : DbContext
public void BuildContextGraph<TContext>(Action<IContextGraphBuilder> builder) where TContext : DbContext
{
BuildContextGraph(builder);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ public static void AddJsonApiInternals(
services.AddScoped<IGenericProcessorFactory, GenericProcessorFactory>();
services.AddScoped(typeof(GenericProcessor<>));
services.AddScoped<IQueryAccessor, QueryAccessor>();
services.AddScoped<IQueryParser, QueryParser>();
services.AddScoped<IControllerContext, Services.ControllerContext>();
}

public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions jsonApiOptions)
Expand Down
180 changes: 0 additions & 180 deletions src/JsonApiDotNetCore/Internal/Query/QuerySet.cs
Original file line number Diff line number Diff line change
@@ -1,193 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JsonApiDotNetCore.Extensions;
using JsonApiDotNetCore.Services;
using Microsoft.AspNetCore.Http;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Controllers;

namespace JsonApiDotNetCore.Internal.Query
{
public class QuerySet
{
private readonly IJsonApiContext _jsonApiContext;

public QuerySet(
IJsonApiContext jsonApiContext,
IQueryCollection query)
{
_jsonApiContext = jsonApiContext;
BuildQuerySet(query);
}

public List<FilterQuery> Filters { get; set; } = new List<FilterQuery>();
public PageQuery PageQuery { get; set; } = new PageQuery();
public List<SortQuery> SortParameters { get; set; } = new List<SortQuery>();
public List<string> IncludedRelationships { get; set; } = new List<string>();
public List<string> Fields { get; set; } = new List<string>();

private void BuildQuerySet(IQueryCollection query)
{
var disabledQueries = _jsonApiContext.GetControllerAttribute<DisableQueryAttribute>()?.QueryParams ?? QueryParams.None;

foreach (var pair in query)
{
if (pair.Key.StartsWith("filter"))
{
if (disabledQueries.HasFlag(QueryParams.Filter) == false)
Filters.AddRange(ParseFilterQuery(pair.Key, pair.Value));
continue;
}

if (pair.Key.StartsWith("sort"))
{
if (disabledQueries.HasFlag(QueryParams.Sort) == false)
SortParameters = ParseSortParameters(pair.Value);
continue;
}

if (pair.Key.StartsWith("include"))
{
if (disabledQueries.HasFlag(QueryParams.Include) == false)
IncludedRelationships = ParseIncludedRelationships(pair.Value);
continue;
}

if (pair.Key.StartsWith("page"))
{
if (disabledQueries.HasFlag(QueryParams.Page) == false)
PageQuery = ParsePageQuery(pair.Key, pair.Value);
continue;
}

if (pair.Key.StartsWith("fields"))
{
if (disabledQueries.HasFlag(QueryParams.Fields) == false)
Fields = ParseFieldsQuery(pair.Key, pair.Value);
continue;
}

if (_jsonApiContext.Options.AllowCustomQueryParameters == false)
throw new JsonApiException(400, $"{pair} is not a valid query.");
}
}

private List<FilterQuery> ParseFilterQuery(string key, string value)
{
// expected input = filter[id]=1
// expected input = filter[id]=eq:1
var queries = new List<FilterQuery>();

var propertyName = key.Split('[', ']')[1].ToProperCase();

var values = value.Split(',');
foreach (var val in values)
{
(var operation, var filterValue) = ParseFilterOperation(val);
queries.Add(new FilterQuery(propertyName, filterValue, operation));
}

return queries;
}

private (string operation, string value) ParseFilterOperation(string value)
{
if (value.Length < 3)
return (string.Empty, value);

var operation = value.Split(':');

if (operation.Length == 1)
return (string.Empty, value);

// remove prefix from value
if (Enum.TryParse(operation[0], out FilterOperations op) == false)
return (string.Empty, value);

var prefix = operation[0];
value = string.Join(":", operation.Skip(1));

return (prefix, value);
}

private PageQuery ParsePageQuery(string key, string value)
{
// expected input = page[size]=10
// page[number]=1
PageQuery = PageQuery ?? new PageQuery();

var propertyName = key.Split('[', ']')[1];

if (propertyName == "size")
PageQuery.PageSize = Convert.ToInt32(value);
else if (propertyName == "number")
PageQuery.PageOffset = Convert.ToInt32(value);

return PageQuery;
}

// sort=id,name
// sort=-id
private List<SortQuery> ParseSortParameters(string value)
{
var sortParameters = new List<SortQuery>();
value.Split(',').ToList().ForEach(p =>
{
var direction = SortDirection.Ascending;
if (p[0] == '-')
{
direction = SortDirection.Descending;
p = p.Substring(1);
}

var attribute = GetAttribute(p.ToProperCase());

sortParameters.Add(new SortQuery(direction, attribute));
});

return sortParameters;
}

private List<string> ParseIncludedRelationships(string value)
{
if (value.Contains("."))
throw new JsonApiException(400, "Deeply nested relationships are not supported");

return value
.Split(',')
.ToList();
}

private List<string> ParseFieldsQuery(string key, string value)
{
// expected: fields[TYPE]=prop1,prop2
var typeName = key.Split('[', ']')[1];

var includedFields = new List<string> { "Id" };

if (typeName != _jsonApiContext.RequestEntity.EntityName)
return includedFields;

var fields = value.Split(',');
foreach (var field in fields)
{
var internalAttrName = _jsonApiContext.RequestEntity
.Attributes
.SingleOrDefault(attr => attr.PublicAttributeName == field)
.InternalAttributeName;
includedFields.Add(internalAttrName);
}

return includedFields;
}

private AttrAttribute GetAttribute(string propertyName)
{
return _jsonApiContext.RequestEntity.Attributes
.FirstOrDefault(attr =>
attr.InternalAttributeName.ToLower() == propertyName.ToLower()
);
}
}
}
2 changes: 1 addition & 1 deletion src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<VersionPrefix>2.1.8</VersionPrefix>
<VersionPrefix>2.1.9</VersionPrefix>
<TargetFrameworks>netstandard1.6</TargetFrameworks>
<AssemblyName>JsonApiDotNetCore</AssemblyName>
<PackageId>JsonApiDotNetCore</PackageId>
Expand Down
25 changes: 25 additions & 0 deletions src/JsonApiDotNetCore/Services/ControllerContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Reflection;
using JsonApiDotNetCore.Internal;

namespace JsonApiDotNetCore.Services
{
public interface IControllerContext
{
Type ControllerType { get; set; }
ContextEntity RequestEntity { get; set; }
TAttribute GetControllerAttribute<TAttribute>() where TAttribute : Attribute;
}

public class ControllerContext : IControllerContext
{
public Type ControllerType { get; set; }
public ContextEntity RequestEntity { get; set; }

public TAttribute GetControllerAttribute<TAttribute>() where TAttribute : Attribute
{
var attribute = ControllerType.GetTypeInfo().GetCustomAttribute(typeof(TAttribute));
return attribute == null ? null : (TAttribute)attribute;
}
}
}
27 changes: 15 additions & 12 deletions src/JsonApiDotNetCore/Services/JsonApiContext.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JsonApiDotNetCore.Builders;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Data;
Expand All @@ -17,26 +16,33 @@ public class JsonApiContext : IJsonApiContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IDbContextResolver _contextResolver;
private readonly IQueryParser _queryParser;
private readonly IControllerContext _controllerContext;

public JsonApiContext(
IDbContextResolver contextResolver,
IContextGraph contextGraph,
IHttpContextAccessor httpContextAccessor,
JsonApiOptions options,
IMetaBuilder metaBuilder,
IGenericProcessorFactory genericProcessorFactory)
IGenericProcessorFactory genericProcessorFactory,
IQueryParser queryParser,
IControllerContext controllerContext)
{
_contextResolver = contextResolver;
ContextGraph = contextGraph;
_httpContextAccessor = httpContextAccessor;
Options = options;
MetaBuilder = metaBuilder;
GenericProcessorFactory = genericProcessorFactory;
_queryParser = queryParser;
_controllerContext = controllerContext;
}

public JsonApiOptions Options { get; set; }
public IContextGraph ContextGraph { get; set; }
public ContextEntity RequestEntity { get; set; }
[Obsolete("Use the proxied member IControllerContext.RequestEntity instead.")]
public ContextEntity RequestEntity { get => _controllerContext.RequestEntity; set => _controllerContext.RequestEntity = value; }
public string BasePath { get; set; }
public QuerySet QuerySet { get; set; }
public bool IsRelationshipData { get; set; }
Expand All @@ -54,21 +60,20 @@ public IJsonApiContext ApplyContext<T>(object controller)
if (controller == null)
throw new JsonApiException(500, $"Cannot ApplyContext from null controller for type {typeof(T)}");

ControllerType = controller.GetType();
_controllerContext.ControllerType = controller.GetType();
_controllerContext.RequestEntity = ContextGraph.GetContextEntity(typeof(T));

var context = _httpContextAccessor.HttpContext;
var path = context.Request.Path.Value.Split('/');

RequestEntity = ContextGraph.GetContextEntity(typeof(T));

if (context.Request.Query.Any())
{
QuerySet = new QuerySet(this, context.Request.Query);
QuerySet = _queryParser.Parse(context.Request.Query);
IncludedRelationships = QuerySet.IncludedRelationships;
}

var linkBuilder = new LinkBuilder(this);
BasePath = linkBuilder.GetBasePath(context, RequestEntity.EntityName);
BasePath = linkBuilder.GetBasePath(context, _controllerContext.RequestEntity.EntityName);
PageManager = GetPageManager();
IsRelationshipPath = path[path.Length - 2] == "relationships";
return this;
Expand All @@ -91,10 +96,8 @@ private PageManager GetPageManager()
};
}

[Obsolete("Use the proxied method IControllerContext.GetControllerAttribute instead.")]
public TAttribute GetControllerAttribute<TAttribute>() where TAttribute : Attribute
{
var attribute = ControllerType.GetTypeInfo().GetCustomAttribute(typeof(TAttribute));
return attribute == null ? null : (TAttribute)attribute;
}
=> _controllerContext.GetControllerAttribute<TAttribute>();
}
}
Loading