diff --git a/.editorconfig b/.editorconfig index 134066baff..3499a1f7a6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,14 @@ charset = utf-8 [*.{csproj,props}] indent_size = 2 + +[*.{cs,vb}] +dotnet_naming_rule.private_members_with_underscore.symbols = private_fields +dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore +dotnet_naming_rule.private_members_with_underscore.severity = suggestion + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +dotnet_naming_style.prefix_underscore.capitalization = camel_case +dotnet_naming_style.prefix_underscore.required_prefix = _ \ No newline at end of file diff --git a/benchmarks/Query/QueryParser_Benchmarks.cs b/benchmarks/Query/QueryParser_Benchmarks.cs index f606b424ed..6cfe71843d 100644 --- a/benchmarks/Query/QueryParser_Benchmarks.cs +++ b/benchmarks/Query/QueryParser_Benchmarks.cs @@ -57,7 +57,7 @@ private void Run(int iterations, Action action) { } // this facade allows us to expose and micro-benchmark protected methods - private class BenchmarkFacade : QueryParser { + private class BenchmarkFacade : QueryParameterDiscovery { public BenchmarkFacade( IRequestContext currentRequest, JsonApiOptions options) : base(currentRequest, options) { } diff --git a/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs b/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs index ed64c98335..2e0a5c0232 100644 --- a/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs +++ b/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs @@ -14,7 +14,7 @@ public class ContainsMediaTypeParameters_Benchmarks [Benchmark] public void Current() - => JsonApiDotNetCore.Middleware.RequestMiddleware.ContainsMediaTypeParameters(MEDIA_TYPE); + => JsonApiDotNetCore.Middleware.CurrentRequestMiddleware.ContainsMediaTypeParameters(MEDIA_TYPE); private bool UsingSplitImpl(string mediaType) { diff --git a/src/Examples/JsonApiDotNetCoreExample/Properties/launchSettings.json b/src/Examples/JsonApiDotNetCoreExample/Properties/launchSettings.json index 0daa3352d1..fa59af8d9d 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Properties/launchSettings.json +++ b/src/Examples/JsonApiDotNetCoreExample/Properties/launchSettings.json @@ -22,7 +22,8 @@ "launchUrl": "http://localhost:5000/api/values", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "applicationUrl": "http://localhost:5000/" } } } \ No newline at end of file diff --git a/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs b/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs index addf1a820e..52497df9a0 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Resources/UserResource.cs @@ -24,13 +24,13 @@ public override QueryFilters GetQueryFilters() private IQueryable FirstCharacterFilter(IQueryable users, FilterQuery filterQuery) { - switch(filterQuery.Operation) - { - case "lt": - return users.Where(u => u.Username[0] < filterQuery.Value[0]); - default: - return users.Where(u => u.Username[0] == filterQuery.Value[0]); - } + switch (filterQuery.Operation) + { + case "lt": + return users.Where(u => u.Username[0] < filterQuery.Value[0]); + default: + return users.Where(u => u.Username[0] == filterQuery.Value[0]); + } } } } diff --git a/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs b/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs index 0035f6855c..4fb672929b 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs @@ -16,12 +16,19 @@ namespace JsonApiDotNetCoreExample.Services { public class CustomArticleService : EntityResourceService
{ - public CustomArticleService(IEntityRepository repository, IJsonApiOptions options, - ITargetedFields updatedFields, ICurrentRequest currentRequest, - IIncludeService includeService, ISparseFieldsService sparseFieldsService, - IPageQueryService pageManager, IResourceGraph resourceGraph, - IResourceHookExecutor hookExecutor = null, ILoggerFactory loggerFactory = null) - : base(repository, options, updatedFields, currentRequest, includeService, sparseFieldsService, pageManager, resourceGraph, hookExecutor, loggerFactory) + public CustomArticleService(ISortService sortService, + IFilterService filterService, + IEntityRepository repository, + IJsonApiOptions options, + ICurrentRequest currentRequest, + IIncludeService includeService, + ISparseFieldsService sparseFieldsService, + IPageService pageManager, + IResourceGraph resourceGraph, + IResourceHookExecutor hookExecutor = null, + ILoggerFactory loggerFactory = null) + : base(sortService, filterService, repository, options, currentRequest, includeService, sparseFieldsService, + pageManager, resourceGraph, hookExecutor, loggerFactory) { } diff --git a/src/Examples/NoEntityFrameworkExample/Properties/launchSettings.json b/src/Examples/NoEntityFrameworkExample/Properties/launchSettings.json index 310f04da95..1dff6cfe69 100644 --- a/src/Examples/NoEntityFrameworkExample/Properties/launchSettings.json +++ b/src/Examples/NoEntityFrameworkExample/Properties/launchSettings.json @@ -8,16 +8,20 @@ } }, "profiles": { + "NoEntityFrameworkExample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5000/" + }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } - }, - "NoEntityFrameworkExample": { - "commandName": "Project", - "environmentVariables": {} } } } \ No newline at end of file diff --git a/src/Examples/ReportsExample/Properties/launchSettings.json b/src/Examples/ReportsExample/Properties/launchSettings.json index 2b84e5d5e8..643dc89799 100644 --- a/src/Examples/ReportsExample/Properties/launchSettings.json +++ b/src/Examples/ReportsExample/Properties/launchSettings.json @@ -8,13 +8,6 @@ } }, "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, "ReportsExample": { "commandName": "Project", "launchBrowser": true, @@ -22,6 +15,13 @@ "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "http://localhost:55654/" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } } } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index f448a4ade6..67e5a45107 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Threading.Tasks; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Extensions; @@ -51,13 +50,8 @@ public BaseJsonApiController( _update = resourceService; _updateRelationships = resourceService; _delete = resourceService; - ParseQueryParams(); } - private void ParseQueryParams() - { - - } public BaseJsonApiController( IJsonApiOptions jsonApiOptions, diff --git a/src/JsonApiDotNetCore/Controllers/DisableQueryAttribute.cs b/src/JsonApiDotNetCore/Controllers/DisableQueryAttribute.cs index 40ebf385fe..d28bd06faf 100644 --- a/src/JsonApiDotNetCore/Controllers/DisableQueryAttribute.cs +++ b/src/JsonApiDotNetCore/Controllers/DisableQueryAttribute.cs @@ -3,12 +3,27 @@ namespace JsonApiDotNetCore.Controllers { public class DisableQueryAttribute : Attribute - { + { + /// + /// Disabled one of the native query parameters for a controller. + /// + /// public DisableQueryAttribute(QueryParams queryParams) { - QueryParams = queryParams; + QueryParams = queryParams.ToString("G").ToLower(); } - public QueryParams QueryParams { get; set; } + /// + /// It is allowed to use strings to indicate which query parameters + /// should be disabled, because the user may have defined a custom + /// query parameter that is not included in the enum. + /// + /// + public DisableQueryAttribute(string customQueryParams) + { + QueryParams = customQueryParams.ToLower(); + } + + public string QueryParams { get; } } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs b/src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs index 889773c5bf..0fa06cab27 100644 --- a/src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs +++ b/src/JsonApiDotNetCore/Controllers/JsonApiControllerMixin.cs @@ -1,8 +1,10 @@ using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Middleware; using Microsoft.AspNetCore.Mvc; namespace JsonApiDotNetCore.Controllers { + [ServiceFilter(typeof(IQueryParameterActionFilter))] public abstract class JsonApiControllerMixin : ControllerBase { protected IActionResult Forbidden() diff --git a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs index 546c96782e..4aa2dfeac9 100644 --- a/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs +++ b/src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs @@ -65,9 +65,9 @@ public DefaultEntityRepository( /// public virtual IQueryable Get() => _dbSet; - + /// - public virtual IQueryable Select(IQueryable entities, List fields) + public virtual IQueryable Select(IQueryable entities, List fields) { if (fields?.Count > 0) return entities.Select(fields); @@ -76,51 +76,36 @@ public virtual IQueryable Select(IQueryable entities, List - public virtual IQueryable Filter(IQueryable entities, FilterQuery filterQuery) + public virtual IQueryable Filter(IQueryable entities, FilterQueryContext filterQueryContext) { - if (_resourceDefinition != null) - { + if (filterQueryContext.IsCustom) + { // todo: consider to move this business logic to service layer + var filterQuery = filterQueryContext.Query; var defaultQueryFilters = _resourceDefinition.GetQueryFilters(); - if (defaultQueryFilters != null && defaultQueryFilters.TryGetValue(filterQuery.Attribute, out var defaultQueryFilter) == true) - { + if (defaultQueryFilters != null && defaultQueryFilters.TryGetValue(filterQuery.Target, out var defaultQueryFilter) == true) return defaultQueryFilter(entities, filterQuery); - } + } - return entities.Filter(new AttrFilterQuery(_currentRequest.GetRequestResource(), _resourceGraph, filterQuery)); + return entities.Filter(filterQueryContext); } /// - public virtual IQueryable Sort(IQueryable entities, List sortQueries) + public virtual IQueryable Sort(IQueryable entities, SortQueryContext sortQueryContext) { - if (sortQueries != null && sortQueries.Count > 0) - return entities.Sort(_currentRequest.GetRequestResource(), _resourceGraph, sortQueries); - - if (_resourceDefinition != null) - { - var defaultSortOrder = _resourceDefinition.DefaultSort(); - if (defaultSortOrder != null && defaultSortOrder.Count > 0) - { - foreach (var sortProp in defaultSortOrder) - { - // this is dumb...add an overload, don't allocate for no reason - entities.Sort(_currentRequest.GetRequestResource(), _resourceGraph, new SortQuery(sortProp.Item2, sortProp.Item1.PublicAttributeName)); - } - } - } - return entities; + return entities.Sort(sortQueryContext); } /// - public virtual async Task GetAsync(TId id) + public virtual async Task GetAsync(TId id, List fields = null) { - return await Select(Get(), _currentRequest.QuerySet?.Fields).SingleOrDefaultAsync(e => e.Id.Equals(id)); + return await Select(Get(), fields).SingleOrDefaultAsync(e => e.Id.Equals(id)); } /// - public virtual async Task GetAndIncludeAsync(TId id, RelationshipAttribute relationship) + public virtual async Task GetAndIncludeAsync(TId id, RelationshipAttribute relationship, List fields = null) { _logger?.LogDebug($"[JADN] GetAndIncludeAsync({id}, {relationship.PublicRelationshipName})"); - var includedSet = Include(Select(Get(), _currentRequest.QuerySet?.Fields), relationship); + var includedSet = Include(Select(Get(), fields), relationship); var result = await includedSet.SingleOrDefaultAsync(e => e.Id.Equals(id)); return result; } @@ -224,12 +209,6 @@ public void DetachRelationshipPointers(TEntity entity) } } - [Obsolete("Use overload UpdateAsync(TEntity updatedEntity): providing parameter ID does no longer add anything relevant")] - public virtual async Task UpdateAsync(TId id, TEntity updatedEntity) - { - return await UpdateAsync(updatedEntity); - } - /// public virtual async Task UpdateAsync(TEntity updatedEntity) { diff --git a/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs b/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs index 57db390f03..5ffb59fe03 100644 --- a/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs +++ b/src/JsonApiDotNetCore/Data/IEntityReadRepository.cs @@ -24,7 +24,7 @@ public interface IEntityReadRepository /// /// Apply fields to the provided queryable /// - IQueryable Select(IQueryable entities, List fields); + IQueryable Select(IQueryable entities, List fields); /// /// Include a relationship in the query @@ -41,12 +41,12 @@ public interface IEntityReadRepository /// /// Apply a filter to the provided queryable /// - IQueryable Filter(IQueryable entities, FilterQuery filterQuery); + IQueryable Filter(IQueryable entities, FilterQueryContext filterQuery); /// /// Apply a sort to the provided queryable /// - IQueryable Sort(IQueryable entities, List sortQueries); + IQueryable Sort(IQueryable entities, SortQueryContext sortQueries); /// /// Paginate the provided queryable @@ -56,7 +56,7 @@ public interface IEntityReadRepository /// /// Get the entity by id /// - Task GetAsync(TId id); + Task GetAsync(TId id, List fields = null); /// /// Get the entity with the specified id and include the relationship. @@ -68,7 +68,7 @@ public interface IEntityReadRepository /// _todoItemsRepository.GetAndIncludeAsync(1, "achieved-date"); /// /// - Task GetAndIncludeAsync(TId id, RelationshipAttribute relationship); + Task GetAndIncludeAsync(TId id, RelationshipAttribute relationship, List fields = null); /// /// Count the total number of records diff --git a/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs b/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs index aa143f53e4..60c632f6a3 100644 --- a/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs +++ b/src/JsonApiDotNetCore/Data/IEntityWriteRepository.cs @@ -17,9 +17,6 @@ public interface IEntityWriteRepository Task UpdateAsync(TEntity entity); - [Obsolete("Use overload UpdateAsync(TEntity updatedEntity): providing parameter ID does no longer add anything relevant")] - Task UpdateAsync(TId id, TEntity entity); - Task UpdateRelationshipsAsync(object parent, RelationshipAttribute relationship, IEnumerable relationshipIds); Task DeleteAsync(TId id); diff --git a/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs b/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs index e4a3aa1c55..3ebe17467d 100644 --- a/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs @@ -21,7 +21,7 @@ public static IApplicationBuilder UseJsonApi(this IApplicationBuilder app, bool app.UseEndpointRouting(); - app.UseMiddleware(); + app.UseMiddleware(); if (useMvc) { diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 66d8683e2b..387243fbaa 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -30,46 +30,57 @@ private static MethodInfo ContainsMethod } } - public static IQueryable Sort(this IQueryable source, ContextEntity primaryResource, IContextEntityProvider provider, List sortQueries) + public static IQueryable PageForward(this IQueryable source, int pageSize, int pageNumber) { - if (sortQueries == null || sortQueries.Count == 0) - return source; - - var orderedEntities = source.Sort(primaryResource, provider, sortQueries[0]); + if (pageSize > 0) + { + if (pageNumber == 0) + pageNumber = 1; - if (sortQueries.Count <= 1) - return orderedEntities; + if (pageNumber > 0) + return source + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize); + } - for (var i = 1; i < sortQueries.Count; i++) - orderedEntities = orderedEntities.Sort(primaryResource, provider, sortQueries[i]); + return source; + } - return orderedEntities; + public static void ForEach(this IEnumerable enumeration, Action action) + { + foreach (T item in enumeration) + { + action(item); + } } - public static IOrderedQueryable Sort(this IQueryable source, ContextEntity primaryResource, IContextEntityProvider provider, SortQuery sortQuery) + public static IQueryable Filter(this IQueryable source, FilterQueryContext filterQuery) { - BaseAttrQuery attr; - if (sortQuery.IsAttributeOfRelationship) - attr = new RelatedAttrSortQuery(primaryResource, provider, sortQuery); - else - attr = new AttrSortQuery(primaryResource, provider, sortQuery); + if (filterQuery == null) + return source; + + if (filterQuery.Operation == FilterOperation.@in || filterQuery.Operation == FilterOperation.nin) + return CallGenericWhereContainsMethod(source, filterQuery); + return CallGenericWhereMethod(source, filterQuery); + } + + public static IQueryable Select(this IQueryable source, List columns) + => CallGenericSelectMethod(source, columns.Select(attr => attr.InternalAttributeName).ToList()); + + public static IOrderedQueryable Sort(this IQueryable source, SortQueryContext sortQuery) + { return sortQuery.Direction == SortDirection.Descending - ? source.OrderByDescending(attr.GetPropertyPath()) - : source.OrderBy(attr.GetPropertyPath()); + ? source.OrderByDescending(sortQuery.GetPropertyPath()) + : source.OrderBy(sortQuery.GetPropertyPath()); } - public static IOrderedQueryable Sort(this IOrderedQueryable source, ContextEntity primaryResource, IContextEntityProvider provider, SortQuery sortQuery) + public static IOrderedQueryable Sort(this IOrderedQueryable source, SortQueryContext sortQuery) { - BaseAttrQuery attr; - if (sortQuery.IsAttributeOfRelationship) - attr = new RelatedAttrSortQuery(primaryResource, provider, sortQuery); - else - attr = new AttrSortQuery(primaryResource, provider, sortQuery); return sortQuery.Direction == SortDirection.Descending - ? source.ThenByDescending(attr.GetPropertyPath()) - : source.ThenBy(attr.GetPropertyPath()); + ? source.ThenByDescending(sortQuery.GetPropertyPath()) + : source.ThenBy(sortQuery.GetPropertyPath()); } public static IOrderedQueryable OrderBy(this IQueryable source, string propertyName) @@ -113,52 +124,39 @@ private static IOrderedQueryable CallGenericOrderMethod(IQuery return (IOrderedQueryable)result; } - - - public static IQueryable Filter(this IQueryable source, BaseFilterQuery filterQuery) - { - if (filterQuery == null) - return source; - - if (filterQuery.FilterOperation == FilterOperations.@in || filterQuery.FilterOperation == FilterOperations.nin) - return CallGenericWhereContainsMethod(source, filterQuery); - else - return CallGenericWhereMethod(source, filterQuery); - } - - private static Expression GetFilterExpressionLambda(Expression left, Expression right, FilterOperations operation) + private static Expression GetFilterExpressionLambda(Expression left, Expression right, FilterOperation operation) { Expression body; switch (operation) { - case FilterOperations.eq: + case FilterOperation.eq: // {model.Id == 1} body = Expression.Equal(left, right); break; - case FilterOperations.lt: + case FilterOperation.lt: // {model.Id < 1} body = Expression.LessThan(left, right); break; - case FilterOperations.gt: + case FilterOperation.gt: // {model.Id > 1} body = Expression.GreaterThan(left, right); break; - case FilterOperations.le: + case FilterOperation.le: // {model.Id <= 1} body = Expression.LessThanOrEqual(left, right); break; - case FilterOperations.ge: + case FilterOperation.ge: // {model.Id >= 1} body = Expression.GreaterThanOrEqual(left, right); break; - case FilterOperations.like: + case FilterOperation.like: body = Expression.Call(left, "Contains", null, right); break; // {model.Id != 1} - case FilterOperations.ne: + case FilterOperation.ne: body = Expression.NotEqual(left, right); break; - case FilterOperations.isnotnull: + case FilterOperation.isnotnull: // {model.Id != null} if (left.Type.IsValueType && !(left.Type.IsGenericType && left.Type.GetGenericTypeDefinition() == typeof(Nullable<>))) @@ -171,7 +169,7 @@ private static Expression GetFilterExpressionLambda(Expression left, Expression body = Expression.NotEqual(left, right); } break; - case FilterOperations.isnull: + case FilterOperation.isnull: // {model.Id == null} if (left.Type.IsValueType && !(left.Type.IsGenericType && left.Type.GetGenericTypeDefinition() == typeof(Nullable<>))) @@ -191,14 +189,14 @@ private static Expression GetFilterExpressionLambda(Expression left, Expression return body; } - private static IQueryable CallGenericWhereContainsMethod(IQueryable source, BaseFilterQuery filter) + private static IQueryable CallGenericWhereContainsMethod(IQueryable source, FilterQueryContext filter) { var concreteType = typeof(TSource); var property = concreteType.GetProperty(filter.Attribute.InternalAttributeName); try { - var propertyValues = filter.PropertyValue.Split(QueryConstants.COMMA); + var propertyValues = filter.Value.Split(QueryConstants.COMMA); ParameterExpression entity = Expression.Parameter(concreteType, "entity"); MemberExpression member; if (filter.IsAttributeOfRelationship) @@ -212,7 +210,7 @@ private static IQueryable CallGenericWhereContainsMethod(IQuer var method = ContainsMethod.MakeGenericMethod(member.Type); var obj = TypeHelper.ConvertListType(propertyValues, member.Type); - if (filter.FilterOperation == FilterOperations.@in) + if (filter.Operation == FilterOperation.@in) { // Where(i => arr.Contains(i.column)) var contains = Expression.Call(method, new Expression[] { Expression.Constant(obj), member }); @@ -231,7 +229,7 @@ private static IQueryable CallGenericWhereContainsMethod(IQuer } catch (FormatException) { - throw new JsonApiException(400, $"Could not cast {filter.PropertyValue} to {property.PropertyType.Name}"); + throw new JsonApiException(400, $"Could not cast {filter.Value} to {property.PropertyType.Name}"); } } @@ -243,9 +241,9 @@ private static IQueryable CallGenericWhereContainsMethod(IQuer /// /// /// - private static IQueryable CallGenericWhereMethod(IQueryable source, BaseFilterQuery filter) + private static IQueryable CallGenericWhereMethod(IQueryable source, FilterQueryContext filter) { - var op = filter.FilterOperation; + var op = filter.Operation; var concreteType = typeof(TSource); PropertyInfo relationProperty = null; PropertyInfo property = null; @@ -283,31 +281,28 @@ private static IQueryable CallGenericWhereMethod(IQueryable 1 - var convertedValue = TypeHelper.ConvertType(filter.PropertyValue, property.PropertyType); + var convertedValue = TypeHelper.ConvertType(filter.Value, property.PropertyType); // {1} right = Expression.Constant(convertedValue, property.PropertyType); } - var body = GetFilterExpressionLambda(left, right, filter.FilterOperation); + var body = GetFilterExpressionLambda(left, right, filter.Operation); var lambda = Expression.Lambda>(body, parameter); return source.Where(lambda); } catch (FormatException) { - throw new JsonApiException(400, $"Could not cast {filter.PropertyValue} to {property.PropertyType.Name}"); + throw new JsonApiException(400, $"Could not cast {filter.Value} to {property.PropertyType.Name}"); } } - public static IQueryable Select(this IQueryable source, List columns) - => CallGenericSelectMethod(source, columns); - private static IQueryable CallGenericSelectMethod(IQueryable source, List columns) { var sourceBindings = new List(); @@ -412,30 +407,5 @@ private static IQueryable CallGenericSelectMethod(IQueryable PageForward(this IQueryable source, int pageSize, int pageNumber) - { - if (pageSize > 0) - { - if (pageNumber == 0) - pageNumber = 1; - - if (pageNumber > 0) - return source - .Skip((pageNumber - 1) * pageSize) - .Take(pageSize); - } - - return source; - } - - public static void ForEach(this IEnumerable enumeration, Action action) - { - foreach (T item in enumeration) - { - action(item); - } - } - } } diff --git a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs index d4433fcb8a..4466e1ae30 100644 --- a/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs @@ -24,6 +24,7 @@ using JsonApiDotNetCore.Serialization.Server.Builders; using JsonApiDotNetCore.Serialization.Server; using JsonApiDotNetCore.Serialization.Client; +using JsonApiDotNetCore.Controllers; namespace JsonApiDotNetCore.Extensions { @@ -127,9 +128,7 @@ private static void AddMvcOptions(MvcOptions options, JsonApiOptions config) { options.Filters.Add(typeof(JsonApiExceptionFilter)); options.Filters.Add(typeof(TypeMatchFilter)); - options.Filters.Add(typeof(JsonApiActionFilter)); options.SerializeAsJsonApi(config); - } public static void AddJsonApiInternals( @@ -184,7 +183,6 @@ public static void AddJsonApiInternals( services.AddScoped(typeof(IResourceService<>), typeof(EntityResourceService<>)); services.AddScoped(typeof(IResourceService<,>), typeof(EntityResourceService<,>)); - services.AddSingleton(jsonApiOptions); services.AddSingleton(jsonApiOptions); services.AddSingleton(graph); @@ -197,12 +195,12 @@ public static void AddJsonApiInternals( services.AddScoped(); services.AddScoped(); services.AddScoped(typeof(GenericProcessor<>)); - services.AddScoped(); - services.AddScoped(); - + services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); + services.AddScoped(); AddServerSerialization(services); AddQueryParameterServices(services); @@ -215,9 +213,20 @@ public static void AddJsonApiInternals( private static void AddQueryParameterServices(IServiceCollection services) { services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); - services.AddScoped(); - + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(sp => sp.GetService()); + services.AddScoped(sp => sp.GetService()); + services.AddScoped(sp => sp.GetService()); + services.AddScoped(sp => sp.GetService()); + services.AddScoped(sp => sp.GetService()); + services.AddScoped(sp => sp.GetService()); + services.AddScoped(sp => sp.GetService()); } diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs deleted file mode 100644 index 5670a01a5f..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/AttrFilterQuery.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Managers.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class AttrFilterQuery : BaseFilterQuery - { - public AttrFilterQuery( - ContextEntity primaryResource, - IContextEntityProvider provider, - FilterQuery filterQuery) - : base(primaryResource, provider, filterQuery) - { - if (Attribute == null) - throw new JsonApiException(400, $"'{filterQuery.Attribute}' is not a valid attribute."); - - if (Attribute.IsFilterable == false) - throw new JsonApiException(400, $"Filter is not allowed for attribute '{Attribute.PublicAttributeName}'."); - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs deleted file mode 100644 index 0b1fdfdf5a..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/AttrSortQuery.cs +++ /dev/null @@ -1,23 +0,0 @@ -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Services; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class AttrSortQuery : BaseAttrQuery - { - public AttrSortQuery(ContextEntity primaryResource, - IContextEntityProvider provider, - SortQuery sortQuery) : base(primaryResource, provider, sortQuery) - { - if (Attribute == null) - throw new JsonApiException(400, $"'{sortQuery.Attribute}' is not a valid attribute."); - - if (Attribute.IsSortable == false) - throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attribute.PublicAttributeName}'."); - - Direction = sortQuery.Direction; - } - - public SortDirection Direction { get; } - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs deleted file mode 100644 index 4746c59e6e..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/BaseAttrQuery.cs +++ /dev/null @@ -1,65 +0,0 @@ -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Models; -using System; -using System.Linq; - -namespace JsonApiDotNetCore.Internal.Query -{ - /// - /// Abstract class to make available shared properties of all query implementations - /// It elimines boilerplate of providing specified type(AttrQuery or RelatedAttrQuery) - /// while filter and sort operations and eliminates plenty of methods to keep DRY principles - /// - public abstract class BaseAttrQuery - { - private readonly IContextEntityProvider _provider; - private readonly ContextEntity _primaryResource; - - public BaseAttrQuery(ContextEntity primaryResource, IContextEntityProvider provider, BaseQuery baseQuery) - { - _provider = provider ?? throw new ArgumentNullException(nameof(provider)); - _primaryResource = primaryResource ?? throw new ArgumentNullException(nameof(primaryResource)); - - - if (baseQuery.IsAttributeOfRelationship) - { - Relationship = GetRelationship(baseQuery.Relationship); - Attribute = GetAttribute(Relationship, baseQuery.Attribute); - } - else - { - Attribute = GetAttribute(baseQuery.Attribute); - } - - } - - public AttrAttribute Attribute { get; } - public RelationshipAttribute Relationship { get; } - public bool IsAttributeOfRelationship => Relationship != null; - - public string GetPropertyPath() - { - if (IsAttributeOfRelationship) - return string.Format("{0}.{1}", Relationship.InternalRelationshipName, Attribute.InternalAttributeName); - else - return Attribute.InternalAttributeName; - } - - private AttrAttribute GetAttribute(string attribute) - { - return _primaryResource.Attributes.FirstOrDefault(attr => attr.Is(attribute)); - } - - private RelationshipAttribute GetRelationship(string propertyName) - { - return _primaryResource.Relationships.FirstOrDefault(r => r.Is(propertyName)); - } - - private AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute) - { - var relatedContextEntity = _provider.GetContextEntity(relationship.DependentType); - return relatedContextEntity.Attributes - .FirstOrDefault(a => a.Is(attribute)); - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs deleted file mode 100644 index bd9588eaa7..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/BaseFilterQuery.cs +++ /dev/null @@ -1,35 +0,0 @@ -using JsonApiDotNetCore.Internal.Contracts; -using System; - -namespace JsonApiDotNetCore.Internal.Query -{ - /// - /// Is the base for all filter queries - /// - public class BaseFilterQuery : BaseAttrQuery - { - public BaseFilterQuery( - ContextEntity primaryResource, - IContextEntityProvider provider, - FilterQuery filterQuery) - : base(primaryResource, provider, filterQuery) - { - PropertyValue = filterQuery.Value; - FilterOperation = GetFilterOperation(filterQuery.Operation); - } - - public string PropertyValue { get; } - public FilterOperations FilterOperation { get; } - - private FilterOperations GetFilterOperation(string prefix) - { - if (prefix.Length == 0) return FilterOperations.eq; - - if (Enum.TryParse(prefix, out FilterOperations opertion) == false) - throw new JsonApiException(400, $"Invalid filter prefix '{prefix}'"); - - return opertion; - } - - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseQuery.cs b/src/JsonApiDotNetCore/Internal/Query/BaseQuery.cs index 90830196c4..75c760ed03 100644 --- a/src/JsonApiDotNetCore/Internal/Query/BaseQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/BaseQuery.cs @@ -1,16 +1,16 @@ -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using System; -using System.Linq; - namespace JsonApiDotNetCore.Internal.Query { + /// + /// represents what FilterQuery and SortQuery have in common: a target. + /// (sort=TARGET, or filter[TARGET]=123). + /// public abstract class BaseQuery { - public BaseQuery(string attribute) + protected BaseQuery(string target) { - var properties = attribute.Split(QueryConstants.DOT); - if(properties.Length > 1) + Target = target; + var properties = target.Split(QueryConstants.DOT); + if (properties.Length > 1) { Relationship = properties[0]; Attribute = properties[1]; @@ -19,8 +19,8 @@ public BaseQuery(string attribute) Attribute = properties[0]; } + public string Target { get; } public string Attribute { get; } public string Relationship { get; } - public bool IsAttributeOfRelationship => Relationship != null; } } diff --git a/src/JsonApiDotNetCore/Internal/Query/BaseQueryContext.cs b/src/JsonApiDotNetCore/Internal/Query/BaseQueryContext.cs new file mode 100644 index 0000000000..75b8dd25db --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/BaseQueryContext.cs @@ -0,0 +1,31 @@ +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Internal.Query +{ + /// + /// A context class that provides extra meta data for a + /// that is used when applying url queries internally. + /// + public abstract class BaseQueryContext where TQuery : BaseQuery + { + protected BaseQueryContext(TQuery query) + { + Query = query; + } + + public bool IsCustom { get; internal set; } + public AttrAttribute Attribute { get; internal set; } + public RelationshipAttribute Relationship { get; internal set; } + public bool IsAttributeOfRelationship => Relationship != null; + + public TQuery Query { get; } + + public string GetPropertyPath() + { + if (IsAttributeOfRelationship) + return string.Format("{0}.{1}", Relationship.InternalRelationshipName, Attribute.InternalAttributeName); + + return Attribute.InternalAttributeName; + } + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs b/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs index 60ae0af012..aee022cd20 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs @@ -1,7 +1,7 @@ // ReSharper disable InconsistentNaming namespace JsonApiDotNetCore.Internal.Query { - public enum FilterOperations + public enum FilterOperation { eq = 0, lt = 1, diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs index e1b53cd47d..e3d2075d36 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs @@ -5,27 +5,21 @@ namespace JsonApiDotNetCore.Internal.Query { /// - /// Allows you to filter the query, via the methods shown at - /// HERE + /// Internal representation of the raw articles?filter[X]=Y query from the URL. /// public class FilterQuery : BaseQuery { - /// - /// Allows you to filter the query, via the methods shown at - /// HERE - /// - /// the json attribute you want to filter on - /// the value this attribute should be - /// possible values: eq, ne, lt, gt, le, ge, like, in (default) - public FilterQuery(string attribute, string value, string operation) - : base(attribute) + public FilterQuery(string target, string value, string operation) + : base(target) { Value = value; Operation = operation; } public string Value { get; set; } + /// + /// See . Can also be a custom operation. + /// public string Operation { get; set; } - } } diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterQueryContext.cs b/src/JsonApiDotNetCore/Internal/Query/FilterQueryContext.cs new file mode 100644 index 0000000000..c4b883292e --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/FilterQueryContext.cs @@ -0,0 +1,24 @@ +using System; + +namespace JsonApiDotNetCore.Internal.Query +{ + /// + /// Wrapper class for filter queries. Provides the internals + /// with metadata it needs to perform the url filter queries on the targeted dataset. + /// + public class FilterQueryContext : BaseQueryContext + { + public FilterQueryContext(FilterQuery query) : base(query) { } + + public string Value => Query.Value; + public FilterOperation Operation + { + get + { + if (!Enum.TryParse(Query.Operation, out var result)) + return FilterOperation.eq; + return result; + } + } + } +} diff --git a/src/JsonApiDotNetCore/Internal/Query/PageQuery.cs b/src/JsonApiDotNetCore/Internal/Query/PageQuery.cs deleted file mode 100644 index eb44cae170..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/PageQuery.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace JsonApiDotNetCore.Internal.Query -{ - public class PageQuery - { - public int? PageSize { get; set; } - public int? PageOffset { get; set; } = 1; - } -} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs b/src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs index 25913ab3e6..14189017da 100644 --- a/src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs +++ b/src/JsonApiDotNetCore/Internal/Query/QueryConstants.cs @@ -1,16 +1,11 @@ -namespace JsonApiDotNetCore.Internal.Query{ +namespace JsonApiDotNetCore.Internal.Query +{ public static class QueryConstants { - public const string FILTER = "filter"; - public const string SORT = "sort"; - public const string INCLUDE = "include"; - public const string PAGE = "page"; - public const string FIELDS = "fields"; public const char OPEN_BRACKET = '['; public const char CLOSE_BRACKET = ']'; public const char COMMA = ','; public const char COLON = ':'; public const string COLON_STR = ":"; public const char DOT = '.'; - } } diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs deleted file mode 100644 index 726810254f..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs +++ /dev/null @@ -1,23 +0,0 @@ -using JsonApiDotNetCore.Internal.Contracts; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class RelatedAttrFilterQuery : BaseFilterQuery - { - public RelatedAttrFilterQuery( - ContextEntity primaryResource, - IContextEntityProvider provider, - FilterQuery filterQuery) - : base(primaryResource, provider, filterQuery) - { - if (Relationship == null) - throw new JsonApiException(400, $"{filterQuery.Relationship} is not a valid relationship on {primaryResource.EntityName}."); - - if (Attribute == null) - throw new JsonApiException(400, $"'{filterQuery.Attribute}' is not a valid attribute."); - - if (Attribute.IsFilterable == false) - throw new JsonApiException(400, $"Filter is not allowed for attribute '{Attribute.PublicAttributeName}'."); - } - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs deleted file mode 100644 index 052c121722..0000000000 --- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrSortQuery.cs +++ /dev/null @@ -1,25 +0,0 @@ -using JsonApiDotNetCore.Internal.Contracts; - -namespace JsonApiDotNetCore.Internal.Query -{ - public class RelatedAttrSortQuery : BaseAttrQuery - { - public RelatedAttrSortQuery(ContextEntity primaryResource, - IContextEntityProvider provider, - SortQuery sortQuery) : base(primaryResource, provider, sortQuery) - { - if (Relationship == null) - throw new JsonApiException(400, $"{sortQuery.Relationship} is not a valid relationship on {primaryResource.EntityName}."); - - if (Attribute == null) - throw new JsonApiException(400, $"'{sortQuery.Attribute}' is not a valid attribute."); - - if (Attribute.IsSortable == false) - throw new JsonApiException(400, $"Sort is not allowed for attribute '{Attribute.PublicAttributeName}'."); - - Direction = sortQuery.Direction; - } - - public SortDirection Direction { get; } - } -} diff --git a/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs b/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs index 7194b6e948..840de80ddb 100644 --- a/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs +++ b/src/JsonApiDotNetCore/Internal/Query/SortQuery.cs @@ -1,15 +1,12 @@ -using JsonApiDotNetCore.Models; -using System; - -namespace JsonApiDotNetCore.Internal.Query +namespace JsonApiDotNetCore.Internal.Query { /// - /// An internal representation of the raw sort query. + /// Internal representation of the raw articles?sort[field] query from the URL. /// public class SortQuery : BaseQuery { - public SortQuery(SortDirection direction, string attribute) - : base(attribute) + public SortQuery(string target, SortDirection direction) + : base(target) { Direction = direction; } diff --git a/src/JsonApiDotNetCore/Internal/Query/SortQueryContext.cs b/src/JsonApiDotNetCore/Internal/Query/SortQueryContext.cs new file mode 100644 index 0000000000..ce1395102d --- /dev/null +++ b/src/JsonApiDotNetCore/Internal/Query/SortQueryContext.cs @@ -0,0 +1,12 @@ +namespace JsonApiDotNetCore.Internal.Query +{ + /// + /// Wrapper class for sort queries. Provides the internals + /// with metadata it needs to perform the url sort queries on the targeted dataset. + /// + public class SortQueryContext : BaseQueryContext + { + public SortQueryContext(SortQuery sortQuery) : base(sortQuery) { } + public SortDirection Direction => Query.Direction; + } +} diff --git a/src/JsonApiDotNetCore/Internal/RouteMatcher.cs b/src/JsonApiDotNetCore/Internal/RouteMatcher.cs deleted file mode 100644 index 4c5771ade1..0000000000 --- a/src/JsonApiDotNetCore/Internal/RouteMatcher.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -namespace JsonApiDotNetCore.Internal -{ - public class RouteMatcher - { - public RouteMatcher() - { - } - } -} diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 97f6b7784d..1ae5427196 100644 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -44,17 +44,4 @@ - - - - - - - - - - - - - diff --git a/src/JsonApiDotNetCore/Middleware/IQueryParameterActionFilter.cs b/src/JsonApiDotNetCore/Middleware/IQueryParameterActionFilter.cs new file mode 100644 index 0000000000..2c843d9d99 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/IQueryParameterActionFilter.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Middleware +{ + public interface IQueryParameterActionFilter + { + Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiActionFilter.cs b/src/JsonApiDotNetCore/Middleware/JsonApiActionFilter.cs deleted file mode 100644 index dfbe7679ca..0000000000 --- a/src/JsonApiDotNetCore/Middleware/JsonApiActionFilter.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.Linq; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Managers.Contracts; -using JsonApiDotNetCore.Query; -using JsonApiDotNetCore.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.Routing; - -namespace JsonApiDotNetCore.Middleware -{ - public class JsonApiActionFilter : IActionFilter - { - private readonly IResourceGraph _resourceGraph; - private readonly ICurrentRequest _currentRequest; - private readonly IPageQueryService _pageManager; - private readonly IQueryParser _queryParser; - private readonly IJsonApiOptions _options; - private HttpContext _httpContext; - public JsonApiActionFilter(IResourceGraph resourceGraph, - ICurrentRequest currentRequest, - IPageQueryService pageManager, - IQueryParser queryParser, - IJsonApiOptions options) - { - _resourceGraph = resourceGraph; - _currentRequest = currentRequest; - _pageManager = pageManager; - _queryParser = queryParser; - _options = options; - } - - /// - /// - public void OnActionExecuting(ActionExecutingContext context) - { - _httpContext = context.HttpContext; - ContextEntity contextEntityCurrent = GetCurrentEntity(); - - // the contextEntity is null eg when we're using a non-JsonApiDotNetCore route. - if (contextEntityCurrent != null) - { - _currentRequest.SetRequestResource(contextEntityCurrent); - _currentRequest.BasePath = GetBasePath(contextEntityCurrent.EntityName); - HandleUriParameters(); - } - - } - - /// - /// Parses the uri - /// - protected void HandleUriParameters() - { - if (_httpContext.Request.Query.Count > 0) - { - _queryParser.Parse(_httpContext.Request.Query); - //_currentRequest.QuerySet = querySet; //this shouldn't be exposed? - //_pageManager.PageSize = querySet.PageQuery.PageSize ?? _pageManager.PageSize; - //_pageManager.CurrentPage = querySet.PageQuery.PageOffset ?? _pageManager.CurrentPage; - } - } - - private string GetBasePath(string entityName) - { - var r = _httpContext.Request; - if (_options.RelativeLinks) - { - return GetNamespaceFromPath(r.Path, entityName); - } - else - { - return $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}"; - } - } - internal static string GetNamespaceFromPath(string path, string entityName) - { - var entityNameSpan = entityName.AsSpan(); - var pathSpan = path.AsSpan(); - const char delimiter = '/'; - for (var i = 0; i < pathSpan.Length; i++) - { - if (pathSpan[i].Equals(delimiter)) - { - var nextPosition = i + 1; - if (pathSpan.Length > i + entityNameSpan.Length) - { - var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length); - if (entityNameSpan.SequenceEqual(possiblePathSegment)) - { - // check to see if it's the last position in the string - // or if the next character is a / - var lastCharacterPosition = nextPosition + entityNameSpan.Length; - - if (lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition].Equals(delimiter)) - { - return pathSpan.Slice(0, i).ToString(); - } - } - } - } - } - - return string.Empty; - } - /// - /// Gets the current entity that we need for serialization and deserialization. - /// - /// - /// - /// - private ContextEntity GetCurrentEntity() - { - var controllerName = (string)_httpContext.GetRouteData().Values["controller"]; - var rd = _httpContext.GetRouteData().Values; - var requestResource = _resourceGraph.GetEntityFromControllerName(controllerName); - - if (rd.TryGetValue("relationshipName", out object relationshipName)) - _currentRequest.RequestRelationship = requestResource.Relationships.Single(r => r.PublicRelationshipName == (string)relationshipName); - return requestResource; - } - - - private bool IsJsonApiRequest(HttpRequest request) - { - return (request.ContentType?.Equals(Constants.ContentType, StringComparison.OrdinalIgnoreCase) == true); - } - - public void OnActionExecuted(ActionExecutedContext context) { /* noop */ } - } -} diff --git a/src/JsonApiDotNetCore/Middleware/QueryParameterFilter.cs b/src/JsonApiDotNetCore/Middleware/QueryParameterFilter.cs new file mode 100644 index 0000000000..9e66363da2 --- /dev/null +++ b/src/JsonApiDotNetCore/Middleware/QueryParameterFilter.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using System.Threading.Tasks; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace JsonApiDotNetCore.Middleware +{ + public class QueryParameterActionFilter : IAsyncActionFilter, IQueryParameterActionFilter + { + private readonly IQueryParameterDiscovery _queryParser; + public QueryParameterActionFilter(IQueryParameterDiscovery queryParser) => _queryParser = queryParser; + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + // gets the DisableQueryAttribute if set on the controller that is targeted by the current request. + DisableQueryAttribute disabledQuery = context.Controller.GetType().GetTypeInfo().GetCustomAttribute(typeof(DisableQueryAttribute)) as DisableQueryAttribute; + + _queryParser.Parse(context.HttpContext.Request.Query, disabledQuery); + await next(); + } + } +} diff --git a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs index 26cd278546..53ace47531 100644 --- a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs +++ b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs @@ -1,7 +1,9 @@ using System; using System.Linq; using System.Threading.Tasks; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Managers.Contracts; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; @@ -14,36 +16,93 @@ namespace JsonApiDotNetCore.Middleware /// /// This sets all necessary parameters relating to the HttpContext for JADNC /// - public class RequestMiddleware + public class CurrentRequestMiddleware { private readonly RequestDelegate _next; private HttpContext _httpContext; private ICurrentRequest _currentRequest; + private IResourceGraph _resourceGraph; + private IJsonApiOptions _options; - public RequestMiddleware(RequestDelegate next) + public CurrentRequestMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext httpContext, - ICurrentRequest currentRequest) - { + IJsonApiOptions options, + ICurrentRequest currentRequest, + IResourceGraph resourceGraph) + { _httpContext = httpContext; _currentRequest = currentRequest; + _resourceGraph = resourceGraph; + _options = options; + var requestResource = GetCurrentEntity(); + if (requestResource != null) + { + _currentRequest.SetRequestResource(GetCurrentEntity()); + _currentRequest.IsRelationshipPath = PathIsRelationship(); + _currentRequest.BasePath = GetBasePath(_currentRequest.GetRequestResource().EntityName); + } if (IsValid()) { - _currentRequest.IsRelationshipPath = PathIsRelationship(); await _next(httpContext); } } + + private string GetBasePath(string entityName) + { + var r = _httpContext.Request; + if (_options.RelativeLinks) + { + return GetNamespaceFromPath(r.Path, entityName); + } + else + { + return $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}"; + } + } + internal static string GetNamespaceFromPath(string path, string entityName) + { + var entityNameSpan = entityName.AsSpan(); + var pathSpan = path.AsSpan(); + const char delimiter = '/'; + for (var i = 0; i < pathSpan.Length; i++) + { + if (pathSpan[i].Equals(delimiter)) + { + var nextPosition = i + 1; + if (pathSpan.Length > i + entityNameSpan.Length) + { + var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length); + if (entityNameSpan.SequenceEqual(possiblePathSegment)) + { + // check to see if it's the last position in the string + // or if the next character is a / + var lastCharacterPosition = nextPosition + entityNameSpan.Length; + + if (lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition].Equals(delimiter)) + { + return pathSpan.Slice(0, i).ToString(); + } + } + } + } + } + + return string.Empty; + } + protected bool PathIsRelationship() { var actionName = (string)_httpContext.GetRouteData().Values["action"]; return actionName.ToLower().Contains("relationships"); } - private bool IsValid() + + private bool IsValid() { return IsValidContentTypeHeader(_httpContext) && IsValidAcceptHeader(_httpContext); } @@ -99,5 +158,22 @@ private void FlushResponse(HttpContext context, int statusCode) context.Response.StatusCode = statusCode; context.Response.Body.Flush(); } + + /// + /// Gets the current entity that we need for serialization and deserialization. + /// + /// + /// + /// + private ContextEntity GetCurrentEntity() + { + var controllerName = (string)_httpContext.GetRouteData().Values["controller"]; + var rd = _httpContext.GetRouteData().Values; + var requestResource = _resourceGraph.GetEntityFromControllerName(controllerName); + + if (rd.TryGetValue("relationshipName", out object relationshipName)) + _currentRequest.RequestRelationship = requestResource.Relationships.Single(r => r.PublicRelationshipName == (string)relationshipName); + return requestResource; + } } } diff --git a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs index 35095a9b22..43b2c4f2f3 100644 --- a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Linq.Expressions; using JsonApiDotNetCore.Services; +using System.Collections; namespace JsonApiDotNetCore.Models { @@ -14,6 +15,8 @@ public interface IResourceDefinition { List GetAllowedAttributes(); List GetAllowedRelationships(); + bool HasCustomQueryFilter(string key); + List<(AttrAttribute, SortDirection)> DefaultSort(); } /// @@ -96,6 +99,11 @@ public void HideFields(Expression> selector) /// public virtual QueryFilters GetQueryFilters() => null; + public bool HasCustomQueryFilter(string key) + { + return GetQueryFilters()?.Keys.Contains(key) ?? false; + } + /// public virtual void AfterCreate(HashSet entities, ResourcePipeline pipeline) { } /// @@ -153,14 +161,7 @@ public class QueryFilters : Dictionary, Filte { var order = new List<(AttrAttribute, SortDirection)>(); foreach (var sortProp in defaultSortOrder) - { - // TODO: error handling, log or throw? - if (sortProp.Item1.Body is MemberExpression memberExpression) - order.Add( - (_contextEntity.Attributes.SingleOrDefault(a => a.InternalAttributeName != memberExpression.Member.Name), - sortProp.Item2) - ); - } + order.Add((_fieldExplorer.GetAttributes(sortProp.Item1).Single(), sortProp.Item2)); return order; } @@ -168,7 +169,6 @@ public class QueryFilters : Dictionary, Filte return null; } - /// /// This is an alias type intended to simplify the implementation's /// method signature. diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Common/IQueryParameterParser.cs b/src/JsonApiDotNetCore/QueryParameterServices/Common/IQueryParameterParser.cs new file mode 100644 index 0000000000..69163e37f4 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Common/IQueryParameterParser.cs @@ -0,0 +1,14 @@ +using JsonApiDotNetCore.Controllers; +using Microsoft.AspNetCore.Http; + +namespace JsonApiDotNetCore.Services +{ + /// + /// Responsible for populating the various service implementations of + /// . + /// + public interface IQueryParameterDiscovery + { + void Parse(IQueryCollection query, DisableQueryAttribute disabledQuery = null); + } +} diff --git a/src/JsonApiDotNetCore/QueryParameters/Common/IQueryParameterService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Common/IQueryParameterService.cs similarity index 52% rename from src/JsonApiDotNetCore/QueryParameters/Common/IQueryParameterService.cs rename to src/JsonApiDotNetCore/QueryParameterServices/Common/IQueryParameterService.cs index 32db65b5f8..64df236abc 100644 --- a/src/JsonApiDotNetCore/QueryParameters/Common/IQueryParameterService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/Common/IQueryParameterService.cs @@ -1,17 +1,20 @@ -namespace JsonApiDotNetCore.Query +using System.Collections.Generic; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Query { /// /// Base interface that all query parameter services should inherit. /// - internal interface IQueryParameterService + public interface IQueryParameterService { /// /// Parses the value of the query parameter. Invoked in the middleware. /// - /// the value of the query parameter as parsed from the url - void Parse(string key, string value); + /// the value of the query parameter as retrieved from the url + void Parse(KeyValuePair queryParameter); /// - /// The name of the query parameter as matched in the URL. + /// The name of the query parameter as matched in the URL query string. /// string Name { get; } } diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Common/QueryParameterParser.cs b/src/JsonApiDotNetCore/QueryParameterServices/Common/QueryParameterParser.cs new file mode 100644 index 0000000000..1e397afbf7 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Common/QueryParameterParser.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Query; +using Microsoft.AspNetCore.Http; + +namespace JsonApiDotNetCore.Services +{ + /// + public class QueryParameterDiscovery : IQueryParameterDiscovery + { + private readonly IJsonApiOptions _options; + private readonly IEnumerable _queryServices; + + public QueryParameterDiscovery(IJsonApiOptions options, IEnumerable queryServices) + { + _options = options; + _queryServices = queryServices; + } + + /// + /// For a query parameter in , calls + /// the + /// method of the corresponding service. + /// + public virtual void Parse(IQueryCollection query, DisableQueryAttribute disabled) + { + var disabledQuery = disabled?.QueryParams; + + foreach (var pair in query) + { + bool parsed = false; + foreach (var service in _queryServices) + { + if (pair.Key.ToLower().StartsWith(service.Name, StringComparison.Ordinal)) + { + if (disabledQuery == null || !IsDisabled(disabledQuery, service)) + service.Parse(pair); + parsed = true; + break; + } + } + if (parsed) + continue; + + if (!_options.AllowCustomQueryParameters) + throw new JsonApiException(400, $"{pair} is not a valid query."); + } + } + + private bool IsDisabled(string disabledQuery, IQueryParameterService targetsService) + { + if (disabledQuery == QueryParams.All.ToString("G").ToLower()) + return true; + + if (disabledQuery == targetsService.Name) + return true; + + return false; + } + } +} diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Common/QueryParameterService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Common/QueryParameterService.cs new file mode 100644 index 0000000000..9673271a94 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Common/QueryParameterService.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Query +{ + /// + /// Base clas for query parameters. + /// + public abstract class QueryParameterService + { + protected readonly IContextEntityProvider _contextEntityProvider; + protected readonly ContextEntity _requestResource; + + protected QueryParameterService(IContextEntityProvider contextEntityProvider, ICurrentRequest currentRequest) + { + _contextEntityProvider = contextEntityProvider; + _requestResource = currentRequest.GetRequestResource(); + } + + protected QueryParameterService() { } + + /// + /// Derives the name of the query parameter from the name of the implementing type. + /// + /// + /// The following query param service will match the query displayed in URL + /// `?include=some-relationship` + /// public class IncludeService : QueryParameterService { /* ... */ } + /// + public virtual string Name { get { return GetParameterNameFromType(); } } + + /// + /// Gets the query parameter name from the implementing class name. Trims "Service" + /// from the name if present. + /// + private string GetParameterNameFromType() => new Regex("Service$").Replace(GetType().Name, string.Empty).ToLower(); + + /// + /// Helper method for parsing query parameters into attributes + /// + protected AttrAttribute GetAttribute(string target, RelationshipAttribute relationship = null) + { + AttrAttribute attribute; + if (relationship != null) + { + var relatedContextEntity = _contextEntityProvider.GetContextEntity(relationship.DependentType); + attribute = relatedContextEntity.Attributes + .FirstOrDefault(a => a.Is(target)); + } + else + { + attribute = _requestResource.Attributes.FirstOrDefault(attr => attr.Is(target)); + } + + if (attribute == null) + throw new JsonApiException(400, $"'{target}' is not a valid attribute."); + + return attribute; + } + + /// + /// Helper method for parsing query parameters into relationships attributes + /// + protected RelationshipAttribute GetRelationship(string propertyName) + { + if (propertyName == null) return null; + var relationship = _requestResource.Relationships.FirstOrDefault(r => r.Is(propertyName)); + if (relationship == null) + throw new JsonApiException(400, $"{propertyName} is not a valid relationship on {_requestResource.EntityName}."); + + return relationship; + } + } +} diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IFilterService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IFilterService.cs new file mode 100644 index 0000000000..02e4d623e8 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IFilterService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Internal.Query; + +namespace JsonApiDotNetCore.Query +{ + /// + /// Query parameter service responsible for url queries of the form ?filter[X]=Y + /// + public interface IFilterService : IQueryParameterService + { + /// + /// Gets the parsed filter queries + /// + List Get(); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/QueryParameters/Contracts/IIncludeService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IIncludeService.cs similarity index 53% rename from src/JsonApiDotNetCore/QueryParameters/Contracts/IIncludeService.cs rename to src/JsonApiDotNetCore/QueryParameterServices/Contracts/IIncludeService.cs index 5e80ef8feb..0de79a3d17 100644 --- a/src/JsonApiDotNetCore/QueryParameters/Contracts/IIncludeService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IIncludeService.cs @@ -4,12 +4,12 @@ namespace JsonApiDotNetCore.Query { /// - /// Query service to access the inclusion chains. + /// Query parameter service responsible for url queries of the form ?include=X.Y.Z,U.V.W /// - public interface IIncludeService + public interface IIncludeService : IQueryParameterService { /// - /// Gets the list of included relationships chains for the current request. + /// Gets the parsed relationship inclusion chains. /// List> Get(); } diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitDefaultService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitDefaultService.cs new file mode 100644 index 0000000000..eab6399407 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitDefaultService.cs @@ -0,0 +1,13 @@ +namespace JsonApiDotNetCore.Query +{ + /// + /// Query parameter service responsible for url queries of the form ?omitDefault=true + /// + public interface IOmitDefaultService : IQueryParameterService + { + /// + /// Gets the parsed config + /// + bool Config { get; } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitNullService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitNullService.cs new file mode 100644 index 0000000000..519d8add42 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IOmitNullService.cs @@ -0,0 +1,13 @@ +namespace JsonApiDotNetCore.Query +{ + /// + /// Query parameter service responsible for url queries of the form ?omitNull=true + /// + public interface IOmitNullService : IQueryParameterService + { + /// + /// Gets the parsed config + /// + bool Config { get; } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/QueryParameters/Contracts/IPageService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IPageService.cs similarity index 80% rename from src/JsonApiDotNetCore/QueryParameters/Contracts/IPageService.cs rename to src/JsonApiDotNetCore/QueryParameterServices/Contracts/IPageService.cs index 0f4db5f4ae..76f56baf6a 100644 --- a/src/JsonApiDotNetCore/QueryParameters/Contracts/IPageService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/IPageService.cs @@ -1,9 +1,9 @@ namespace JsonApiDotNetCore.Query { /// - /// The former page manager. Needs some work. + /// Query parameter service responsible for url queries of the form ?page[size]=X&page[number]=Y /// - public interface IPageQueryService + public interface IPageService : IQueryParameterService { /// /// What the total records are for this output @@ -28,7 +28,7 @@ public interface IPageQueryService int TotalPages { get; } /// - /// Pagination is enabled + /// Checks if pagination is enabled /// bool ShouldPaginate(); } diff --git a/src/JsonApiDotNetCore/QueryParameterServices/Contracts/ISortService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/ISortService.cs new file mode 100644 index 0000000000..781da03713 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/ISortService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Internal.Query; + +namespace JsonApiDotNetCore.Query +{ + /// + /// Query parameter service responsible for url queries of the form ?sort=-X + /// + public interface ISortService : IQueryParameterService + { + /// + /// Gets the parsed sort queries + /// + List Get(); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/QueryParameters/Contracts/ISparseFieldsService.cs b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/ISparseFieldsService.cs similarity index 64% rename from src/JsonApiDotNetCore/QueryParameters/Contracts/ISparseFieldsService.cs rename to src/JsonApiDotNetCore/QueryParameterServices/Contracts/ISparseFieldsService.cs index cc339ae36c..a5879d7595 100644 --- a/src/JsonApiDotNetCore/QueryParameters/Contracts/ISparseFieldsService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/Contracts/ISparseFieldsService.cs @@ -4,16 +4,15 @@ namespace JsonApiDotNetCore.Query { /// - /// Query service to access sparse field selection. + /// Query parameter service responsible for url queries of the form ?fields[X]=U,V,W /// - public interface ISparseFieldsService + public interface ISparseFieldsService : IQueryParameterService { /// - /// Gets the list of targeted fields. In a relationship is supplied, + /// Gets the list of targeted fields. If a relationship is supplied, /// gets the list of targeted fields for that relationship. /// /// - /// List Get(RelationshipAttribute relationship = null); } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/QueryParameterServices/FilterService.cs b/src/JsonApiDotNetCore/QueryParameterServices/FilterService.cs new file mode 100644 index 0000000000..8e723ce001 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/FilterService.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Query +{ + /// + public class FilterService : QueryParameterService, IFilterService + { + private readonly List _filters; + private IResourceDefinition _requestResourceDefinition; + + public FilterService(IResourceDefinitionProvider resourceDefinitionProvider, IContextEntityProvider contextEntityProvider, ICurrentRequest currentRequest) : base(contextEntityProvider, currentRequest) + { + _requestResourceDefinition = resourceDefinitionProvider.Get(_requestResource.EntityType); + _filters = new List(); + } + + /// + public List Get() + { + return _filters; + } + + /// + public virtual void Parse(KeyValuePair queryParameter) + { + var queries = GetFilterQueries(queryParameter); + _filters.AddRange(queries.Select(GetQueryContexts)); + } + + private FilterQueryContext GetQueryContexts(FilterQuery query) + { + var queryContext = new FilterQueryContext(query); + if (_requestResourceDefinition != null && _requestResourceDefinition.HasCustomQueryFilter(query.Target)) + { + queryContext.IsCustom = true; + return queryContext; + } + + queryContext.Relationship = GetRelationship(query.Relationship); + var attribute = GetAttribute(query.Attribute, queryContext.Relationship); + + if (attribute.IsFilterable == false) + throw new JsonApiException(400, $"Filter is not allowed for attribute '{attribute.PublicAttributeName}'."); + queryContext.Attribute = attribute; + + return queryContext; + } + + /// todo: this could be simplified a bunch + private List GetFilterQueries(KeyValuePair queryParameter) + { + // expected input = filter[id]=1 + // expected input = filter[id]=eq:1 + var propertyName = queryParameter.Key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; + var queries = new List(); + // InArray case + string op = GetFilterOperation(queryParameter.Value); + if (string.Equals(op, FilterOperation.@in.ToString(), StringComparison.OrdinalIgnoreCase) + || string.Equals(op, FilterOperation.nin.ToString(), StringComparison.OrdinalIgnoreCase)) + { + (var _, var filterValue) = ParseFilterOperation(queryParameter.Value); + queries.Add(new FilterQuery(propertyName, filterValue, op)); + } + else + { + var values = ((string)queryParameter.Value).Split(QueryConstants.COMMA); + foreach (var val in values) + { + (var operation, var filterValue) = ParseFilterOperation(val); + queries.Add(new FilterQuery(propertyName, filterValue, operation)); + } + } + return queries; + } + + /// todo: this could be simplified a bunch + private (string operation, string value) ParseFilterOperation(string value) + { + if (value.Length < 3) + return (string.Empty, value); + + var operation = GetFilterOperation(value); + var values = value.Split(QueryConstants.COLON); + + if (string.IsNullOrEmpty(operation)) + return (string.Empty, value); + + value = string.Join(QueryConstants.COLON_STR, values.Skip(1)); + + return (operation, value); + } + + /// todo: this could be simplified a bunch + private string GetFilterOperation(string value) + { + var values = value.Split(QueryConstants.COLON); + + if (values.Length == 1) + return string.Empty; + + var operation = values[0]; + // remove prefix from value + if (Enum.TryParse(operation, out FilterOperation op) == false) + return string.Empty; + + return operation; + } + } +} diff --git a/src/JsonApiDotNetCore/QueryParameters/IncludeService.cs b/src/JsonApiDotNetCore/QueryParameterServices/IncludeService.cs similarity index 71% rename from src/JsonApiDotNetCore/QueryParameters/IncludeService.cs rename to src/JsonApiDotNetCore/QueryParameterServices/IncludeService.cs index 57a94eef66..c68c6f4634 100644 --- a/src/JsonApiDotNetCore/QueryParameters/IncludeService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/IncludeService.cs @@ -5,30 +5,18 @@ using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; +using Microsoft.Extensions.Primitives; namespace JsonApiDotNetCore.Query { - public class IncludeService : QueryParameterService, IIncludeService { /// todo: use read-only lists. private readonly List> _includedChains; - private readonly ICurrentRequest _currentRequest; - private readonly IContextEntityProvider _provider; - private ContextEntity _primaryResourceContext; - public IncludeService(ICurrentRequest currentRequest, IContextEntityProvider provider) - { - _currentRequest = currentRequest; - _provider = provider; - _includedChains = new List>(); - } - /// - /// This constructor is used internally for testing. - /// - internal IncludeService(ContextEntity primaryResourceContext, IContextEntityProvider provider) : this(currentRequest: null, provider: provider) + public IncludeService(IContextEntityProvider contextEntityProvider, ICurrentRequest currentRequest) : base(contextEntityProvider, currentRequest) { - _primaryResourceContext = primaryResourceContext; + _includedChains = new List>(); } /// @@ -38,8 +26,9 @@ public List> Get() } /// - public override void Parse(string _, string value) + public virtual void Parse(KeyValuePair queryParameter) { + var value = (string)queryParameter.Value; if (string.IsNullOrWhiteSpace(value)) throw new JsonApiException(400, "Include parameter must not be empty if provided"); @@ -50,11 +39,9 @@ public override void Parse(string _, string value) private void ParseChain(string chain) { - _primaryResourceContext = _primaryResourceContext ?? _currentRequest.GetRequestResource(); - var parsedChain = new List(); var chainParts = chain.Split(QueryConstants.DOT); - var resourceContext = _primaryResourceContext; + var resourceContext = _requestResource; foreach (var relationshipName in chainParts) { var relationship = resourceContext.Relationships.SingleOrDefault(r => r.PublicRelationshipName == relationshipName); @@ -65,7 +52,7 @@ private void ParseChain(string chain) throw CannotIncludeError(resourceContext, relationshipName); parsedChain.Add(relationship); - resourceContext = _provider.GetContextEntity(relationship.DependentType); + resourceContext = _contextEntityProvider.GetContextEntity(relationship.DependentType); } _includedChains.Add(parsedChain); } diff --git a/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs b/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs new file mode 100644 index 0000000000..0887f414b0 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/OmitDefaultService.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Query +{ + /// + public class OmitDefaultService : QueryParameterService, IOmitDefaultService + { + private readonly IJsonApiOptions _options; + + public OmitDefaultService(IJsonApiOptions options) + { + Config = options.DefaultAttributeResponseBehavior.OmitDefaultValuedAttributes; + _options = options; + } + + /// + public bool Config { get; private set; } + + /// + public virtual void Parse(KeyValuePair queryParameter) + { + if (!_options.DefaultAttributeResponseBehavior.AllowClientOverride) + return; + + if (!bool.TryParse(queryParameter.Value, out var config)) + return; + + Config = config; + } + } +} diff --git a/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs b/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs new file mode 100644 index 0000000000..57d69866af --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/OmitNullService.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Query +{ + /// + public class OmitNullService : QueryParameterService, IOmitNullService + { + private readonly IJsonApiOptions _options; + + public OmitNullService(IJsonApiOptions options) + { + Config = options.NullAttributeResponseBehavior.OmitNullValuedAttributes; + _options = options; + } + + /// + public bool Config { get; private set; } + + /// + public virtual void Parse(KeyValuePair queryParameter) + { + if (!_options.NullAttributeResponseBehavior.AllowClientOverride) + return; + + if (!bool.TryParse(queryParameter.Value, out var config)) + return; + + Config = config; + } + } +} diff --git a/src/JsonApiDotNetCore/QueryParameterServices/PageService.cs b/src/JsonApiDotNetCore/QueryParameterServices/PageService.cs new file mode 100644 index 0000000000..d4aa5052ee --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/PageService.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Query; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Query +{ + /// + public class PageService : QueryParameterService, IPageService + { + private IJsonApiOptions _options; + + public PageService(IJsonApiOptions options) + { + _options = options; + DefaultPageSize = _options.DefaultPageSize; + PageSize = _options.DefaultPageSize; + } + /// + public int? TotalRecords { get; set; } + /// + public int PageSize { get; set; } + /// + public int DefaultPageSize { get; set; } // I think we shouldnt expose this + /// + public int CurrentPage { get; set; } + /// + public int TotalPages => (TotalRecords == null) ? -1 : (int)Math.Ceiling(decimal.Divide(TotalRecords.Value, PageSize)); + + /// + public virtual void Parse(KeyValuePair queryParameter) + { + // expected input = page[size]=10 + // page[number]=1 + var propertyName = queryParameter.Key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; + + const string SIZE = "size"; + const string NUMBER = "number"; + + if (propertyName == SIZE) + { + if (int.TryParse(queryParameter.Value, out var size)) + PageSize = size; + else + throw new JsonApiException(400, $"Invalid page size '{queryParameter.Value}'"); + } + else if (propertyName == NUMBER) + { + if (int.TryParse(queryParameter.Value, out var size)) + CurrentPage = size; + else + throw new JsonApiException(400, $"Invalid page number '{queryParameter.Value}'"); + } + } + + /// + public bool ShouldPaginate() + { + return (PageSize > 0) || ((CurrentPage == 1 || CurrentPage == 0) && TotalPages <= 0); + } + } +} diff --git a/src/JsonApiDotNetCore/QueryParameterServices/SortService.cs b/src/JsonApiDotNetCore/QueryParameterServices/SortService.cs new file mode 100644 index 0000000000..da2dbd7a34 --- /dev/null +++ b/src/JsonApiDotNetCore/QueryParameterServices/SortService.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Managers.Contracts; +using Microsoft.Extensions.Primitives; + +namespace JsonApiDotNetCore.Query +{ + /// + public class SortService : QueryParameterService, ISortService + { + const char DESCENDING_SORT_OPERATOR = '-'; + private readonly IResourceDefinitionProvider _resourceDefinitionProvider; + private List _queries; + private bool _isProcessed; + + public SortService(IResourceDefinitionProvider resourceDefinitionProvider, + IContextEntityProvider contextEntityProvider, + ICurrentRequest currentRequest) + : base(contextEntityProvider, currentRequest) + { + _resourceDefinitionProvider = resourceDefinitionProvider; + _queries = new List(); + } + + /// + public virtual void Parse(KeyValuePair queryParameter) + { + CheckIfProcessed(); // disallow multiple sort parameters. + var queries = BuildQueries(queryParameter.Value); + + _queries = queries.Select(BuildQueryContext).ToList(); + } + + /// + public List Get() + { + if (_queries == null) + { + var requestResourceDefinition = _resourceDefinitionProvider.Get(_requestResource.EntityType); + if (requestResourceDefinition != null) + return requestResourceDefinition.DefaultSort()?.Select(d => BuildQueryContext(new SortQuery(d.Item1.PublicAttributeName, d.Item2))).ToList(); + } + return _queries.ToList(); + } + + private List BuildQueries(string value) + { + var sortParameters = new List(); + + var sortSegments = value.Split(QueryConstants.COMMA); + if (sortSegments.Any(s => s == string.Empty)) + throw new JsonApiException(400, "The sort URI segment contained a null value."); + + foreach (var sortSegment in sortSegments) + { + var propertyName = sortSegment; + var direction = SortDirection.Ascending; + + if (sortSegment[0] == DESCENDING_SORT_OPERATOR) + { + direction = SortDirection.Descending; + propertyName = propertyName.Substring(1); + } + + sortParameters.Add(new SortQuery(propertyName, direction)); + } + + return sortParameters; + } + + private SortQueryContext BuildQueryContext(SortQuery query) + { + var relationship = GetRelationship(query.Relationship); + var attribute = GetAttribute(query.Attribute, relationship); + + if (attribute.IsSortable == false) + throw new JsonApiException(400, $"Sort is not allowed for attribute '{attribute.PublicAttributeName}'."); + + return new SortQueryContext(query) + { + Attribute = attribute, + Relationship = relationship + }; + } + + private void CheckIfProcessed() + { + if (_isProcessed) + throw new JsonApiException(400, "The sort query parameter occured in the URI more than once."); + + _isProcessed = true; + } + + } +} diff --git a/src/JsonApiDotNetCore/QueryParameters/SparseFieldsService.cs b/src/JsonApiDotNetCore/QueryParameterServices/SparseFieldsService.cs similarity index 69% rename from src/JsonApiDotNetCore/QueryParameters/SparseFieldsService.cs rename to src/JsonApiDotNetCore/QueryParameterServices/SparseFieldsService.cs index 2873daab16..c3a26fd983 100644 --- a/src/JsonApiDotNetCore/QueryParameters/SparseFieldsService.cs +++ b/src/JsonApiDotNetCore/QueryParameterServices/SparseFieldsService.cs @@ -6,9 +6,9 @@ using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; +using Microsoft.Extensions.Primitives; namespace JsonApiDotNetCore.Query - { /// public class SparseFieldsService : QueryParameterService, ISparseFieldsService @@ -21,15 +21,13 @@ public class SparseFieldsService : QueryParameterService, ISparseFieldsService /// The selected field for any included relationships /// private readonly Dictionary> _selectedRelationshipFields; - private readonly ICurrentRequest _currentRequest; - private readonly IContextEntityProvider _provider; - public SparseFieldsService(ICurrentRequest currentRequest, IContextEntityProvider provider) + public override string Name => "fields"; + + public SparseFieldsService(IContextEntityProvider contextEntityProvider, ICurrentRequest currentRequest) : base(contextEntityProvider, currentRequest) { _selectedFields = new List(); _selectedRelationshipFields = new Dictionary>(); - _currentRequest = currentRequest; - _provider = provider; } /// @@ -43,24 +41,22 @@ public List Get(RelationshipAttribute relationship = null) } /// - public override void Parse(string key, string value) + public virtual void Parse(KeyValuePair queryParameter) { - var primaryResource = _currentRequest.GetRequestResource(); - // expected: fields[TYPE]=prop1,prop2 - var typeName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; + var typeName = queryParameter.Key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; var includedFields = new List { nameof(Identifiable.Id) }; - var relationship = primaryResource.Relationships.SingleOrDefault(a => a.Is(typeName)); - if (relationship == null && string.Equals(typeName, primaryResource.EntityName, StringComparison.OrdinalIgnoreCase) == false) + var relationship = _requestResource.Relationships.SingleOrDefault(a => a.Is(typeName)); + if (relationship == null && string.Equals(typeName, _requestResource.EntityName, StringComparison.OrdinalIgnoreCase) == false) throw new JsonApiException(400, $"fields[{typeName}] is invalid"); - var fields = value.Split(QueryConstants.COMMA); + var fields = ((string)queryParameter.Value).Split(QueryConstants.COMMA); foreach (var field in fields) { if (relationship != default) { - var relationProperty = _provider.GetContextEntity(relationship.DependentType); + var relationProperty = _contextEntityProvider.GetContextEntity(relationship.DependentType); var attr = relationProperty.Attributes.SingleOrDefault(a => a.Is(field)); if (attr == null) throw new JsonApiException(400, $"'{relationship.DependentType.Name}' does not contain '{field}'."); @@ -71,9 +67,9 @@ public override void Parse(string key, string value) } else { - var attr = primaryResource.Attributes.SingleOrDefault(a => a.Is(field)); + var attr = _requestResource.Attributes.SingleOrDefault(a => a.Is(field)); if (attr == null) - throw new JsonApiException(400, $"'{primaryResource.EntityName}' does not contain '{field}'."); + throw new JsonApiException(400, $"'{_requestResource.EntityName}' does not contain '{field}'."); (_selectedFields = _selectedFields ?? new List()).Add(attr); } diff --git a/src/JsonApiDotNetCore/QueryParameters/Common/QueryParameterService.cs b/src/JsonApiDotNetCore/QueryParameters/Common/QueryParameterService.cs deleted file mode 100644 index 22dde49194..0000000000 --- a/src/JsonApiDotNetCore/QueryParameters/Common/QueryParameterService.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Text.RegularExpressions; - -namespace JsonApiDotNetCore.Query -{ - public abstract class QueryParameterService : IQueryParameterService - { - - /// - /// By default, the name is derived from the implementing type. - /// - /// - /// The following query param service will match the query displayed in URL - /// `?include=some-relationship` - /// public class IncludeService : QueryParameterService { /* ... */ } - /// - public virtual string Name { get { return GetParameterNameFromType(); } } - - /// - public abstract void Parse(string key, string value); - - /// - /// Gets the query parameter name from the implementing class name. Trims "Service" - /// from the name if present. - /// - private string GetParameterNameFromType() => new Regex("Service$").Replace(GetType().Name, string.Empty); - } -} diff --git a/src/JsonApiDotNetCore/QueryParameters/Contracts/IAttributeBehaviourService.cs b/src/JsonApiDotNetCore/QueryParameters/Contracts/IAttributeBehaviourService.cs deleted file mode 100644 index 109866bc85..0000000000 --- a/src/JsonApiDotNetCore/QueryParameters/Contracts/IAttributeBehaviourService.cs +++ /dev/null @@ -1,20 +0,0 @@ -using JsonApiDotNetCore.Serialization; - -namespace JsonApiDotNetCore.Query -{ - /// - /// Encapsulates client overrides of omit null and omit default values behaviour - /// in - /// - public interface IAttributeBehaviourService - { - /// - /// Value of client query param overriding the omit null values behaviour in the server serializer - /// - bool? OmitNullValuedAttributes { get; set; } - /// - /// Value of client query param overriding the omit default values behaviour in the server serializer - /// - bool? OmitDefaultValuedAttributes { get; set; } - } -} diff --git a/src/JsonApiDotNetCore/QueryParameters/FilterService.cs b/src/JsonApiDotNetCore/QueryParameters/FilterService.cs deleted file mode 100644 index 92876ad806..0000000000 --- a/src/JsonApiDotNetCore/QueryParameters/FilterService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace JsonApiDotNetCore.Query -{ - public class FilterService : QueryParameterService - { - public override void Parse(string key, string value) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/JsonApiDotNetCore/QueryParameters/OmitDefaultValuedAttributesService.cs b/src/JsonApiDotNetCore/QueryParameters/OmitDefaultValuedAttributesService.cs deleted file mode 100644 index 282e0207aa..0000000000 --- a/src/JsonApiDotNetCore/QueryParameters/OmitDefaultValuedAttributesService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace JsonApiDotNetCore.Query -{ - public class OmitDefaultService : QueryParameterService - { - public override void Parse(string key, string value) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/JsonApiDotNetCore/QueryParameters/OmitNullValuedAttributesService.cs b/src/JsonApiDotNetCore/QueryParameters/OmitNullValuedAttributesService.cs deleted file mode 100644 index ac23fdcc3f..0000000000 --- a/src/JsonApiDotNetCore/QueryParameters/OmitNullValuedAttributesService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace JsonApiDotNetCore.Query -{ - public class OmitNullService : QueryParameterService - { - public override void Parse(string key, string value) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/JsonApiDotNetCore/QueryParameters/PageService.cs b/src/JsonApiDotNetCore/QueryParameters/PageService.cs deleted file mode 100644 index 35ec0ade5d..0000000000 --- a/src/JsonApiDotNetCore/QueryParameters/PageService.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Query; - -namespace JsonApiDotNetCore.Query - -{ - public class PageService : QueryParameterService, IPageQueryService - { - private IJsonApiOptions _options; - - public PageService(IJsonApiOptions options) - { - _options = options; - DefaultPageSize = _options.DefaultPageSize; - PageSize = _options.DefaultPageSize; - } - /// - public int? TotalRecords { get; set; } - /// - public int PageSize { get; set; } - /// - public int DefaultPageSize { get; set; } // I think we shouldnt expose this - /// - public int CurrentPage { get; set; } - /// - public int TotalPages => (TotalRecords == null) ? -1 : (int)Math.Ceiling(decimal.Divide(TotalRecords.Value, PageSize)); - - public override void Parse(string key, string value) - { - throw new NotImplementedException(); - } - - - /// - public bool ShouldPaginate() - { - return (PageSize > 0) || ((CurrentPage == 1 || CurrentPage == 0) && TotalPages <= 0); - } - } -} diff --git a/src/JsonApiDotNetCore/QueryParameters/SortService.cs b/src/JsonApiDotNetCore/QueryParameters/SortService.cs deleted file mode 100644 index 064f25e2ac..0000000000 --- a/src/JsonApiDotNetCore/QueryParameters/SortService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace JsonApiDotNetCore.Query -{ - public class SortService : QueryParameterService - { - public override void Parse(string key, string value) - { - throw new NotImplementedException(); - } - } -} diff --git a/src/JsonApiDotNetCore/RequestServices/Contracts/ICurrentRequest.cs b/src/JsonApiDotNetCore/RequestServices/Contracts/ICurrentRequest.cs index e215d64aa1..ac65ffbc2c 100644 --- a/src/JsonApiDotNetCore/RequestServices/Contracts/ICurrentRequest.cs +++ b/src/JsonApiDotNetCore/RequestServices/Contracts/ICurrentRequest.cs @@ -1,10 +1,6 @@ -using System.Collections.Generic; using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Query; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Services; -using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.Managers.Contracts { @@ -24,8 +20,6 @@ public interface ICurrentRequest /// Relative: /api/v1 /// string BasePath { get; set; } - QuerySet QuerySet { get; set; } - IQueryCollection FullQuerySet { get; set; } /// /// If the request is on the `{id}/relationships/{relationshipName}` route @@ -45,10 +39,5 @@ public interface ICurrentRequest void SetRequestResource(ContextEntity contextEntityCurrent); ContextEntity GetRequestResource(); - /// - /// Which query params are filtered - /// - QueryParams DisabledQueryParams { get; set; } - } } diff --git a/src/JsonApiDotNetCore/RequestServices/Contracts/QuerySet.cs b/src/JsonApiDotNetCore/RequestServices/Contracts/QuerySet.cs deleted file mode 100644 index 3d5c387dc8..0000000000 --- a/src/JsonApiDotNetCore/RequestServices/Contracts/QuerySet.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Internal.Query; - -namespace JsonApiDotNetCore.Managers.Contracts -{ - public class QuerySet - { - public List Filters { get; internal set; } - public List Fields { get; internal set; } - public List SortParameters { get; internal set; } - } -} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/RequestServices/CurrentRequest.cs b/src/JsonApiDotNetCore/RequestServices/CurrentRequest.cs index 3da1c7a9be..bbaf8c037c 100644 --- a/src/JsonApiDotNetCore/RequestServices/CurrentRequest.cs +++ b/src/JsonApiDotNetCore/RequestServices/CurrentRequest.cs @@ -1,29 +1,15 @@ -using JsonApiDotNetCore.Controllers; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Managers.Contracts; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Query; -using Microsoft.AspNetCore.Http; -using System.Collections.Generic; namespace JsonApiDotNetCore.Managers { - class CurrentRequest : ICurrentRequest { private ContextEntity _contextEntity; public string BasePath { get; set; } - public List IncludedRelationships { get; set; } - public PageService PageManager { get; set; } - public IQueryCollection FullQuerySet { get; set; } - public QueryParams DisabledQueryParams { get; set; } public bool IsRelationshipPath { get; set; } - public Dictionary AttributesToUpdate { get; set; } - - public Dictionary RelationshipsToUpdate { get; set; } - public RelationshipAttribute RequestRelationship { get; set; } - public QuerySet QuerySet { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); } /// /// The main resource of the request. diff --git a/src/JsonApiDotNetCore/RequestServices/UpdatedFields.cs b/src/JsonApiDotNetCore/RequestServices/TargetedFields.cs similarity index 100% rename from src/JsonApiDotNetCore/RequestServices/UpdatedFields.cs rename to src/JsonApiDotNetCore/RequestServices/TargetedFields.cs diff --git a/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilder.cs b/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilder.cs index d5286fccc3..f57d954f46 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/ResourceObjectBuilder.cs @@ -142,7 +142,7 @@ private void ProcessAttributes(IIdentifiable entity, IEnumerable foreach (var attr in attributes) { var value = attr.GetValue(entity); - if (!(value == default && _settings.OmitDefaultValuedAttributes) && !(value == null && _settings.OmitDefaultValuedAttributes)) + if (!(value == default && _settings.OmitDefaultValuedAttributes) && !(value == null && _settings.OmitNullValuedAttributes)) ro.Attributes.Add(attr.PublicAttributeName, value); } } diff --git a/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs index 58ca2d09a9..1139616417 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs @@ -5,21 +5,19 @@ using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.Links; using JsonApiDotNetCore.Query; -using JsonApiDotNetCore.Services; namespace JsonApiDotNetCore.Serialization.Server.Builders { - public class LinkBuilder : ILinkBuilder { private readonly ICurrentRequest _currentRequest; private readonly ILinksConfiguration _options; private readonly IContextEntityProvider _provider; - private readonly IPageQueryService _pageManager; + private readonly IPageService _pageManager; public LinkBuilder(ILinksConfiguration options, ICurrentRequest currentRequest, - IPageQueryService pageManager, + IPageService pageManager, IContextEntityProvider provider) { _options = options; @@ -67,7 +65,6 @@ private void SetPageLinks(ContextEntity primaryResource, ref TopLevelLinks links links.Prev = GetPageLink(primaryResource, _pageManager.CurrentPage - 1, _pageManager.PageSize); } - if (_pageManager.CurrentPage < _pageManager.TotalPages) links.Next = GetPageLink(primaryResource, _pageManager.CurrentPage + 1, _pageManager.PageSize); @@ -83,9 +80,7 @@ private string GetSelfTopLevelLink(string resourceName) private string GetPageLink(ContextEntity primaryResource, int pageOffset, int pageSize) { - var filterQueryComposer = new QueryComposer(); - var filters = filterQueryComposer.Compose(_currentRequest); - return $"{GetBasePath()}/{primaryResource.EntityName}?page[size]={pageSize}&page[number]={pageOffset}{filters}"; + return $"{GetBasePath()}/{primaryResource.EntityName}?page[size]={pageSize}&page[number]={pageOffset}"; } diff --git a/src/JsonApiDotNetCore/Serialization/Server/Builders/MetaBuilder.cs b/src/JsonApiDotNetCore/Serialization/Server/Builders/MetaBuilder.cs index 38c5abc423..aeacf82987 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Builders/MetaBuilder.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/Builders/MetaBuilder.cs @@ -11,12 +11,12 @@ namespace JsonApiDotNetCore.Serialization.Server.Builders public class MetaBuilder : IMetaBuilder where T : class, IIdentifiable { private Dictionary _meta = new Dictionary(); - private readonly IPageQueryService _pageManager; + private readonly IPageService _pageManager; private readonly IJsonApiOptions _options; private readonly IRequestMeta _requestMeta; private readonly IHasMeta _resourceMeta; - public MetaBuilder(IPageQueryService pageManager, + public MetaBuilder(IPageService pageManager, IJsonApiOptions options, IRequestMeta requestMeta = null, ResourceDefinition resourceDefinition = null) diff --git a/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs b/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs index fdde58d4d3..9e4541c201 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/ResourceObjectBuilderSettingsProvider.cs @@ -9,29 +9,20 @@ namespace JsonApiDotNetCore.Serialization.Server /// public class ResourceObjectBuilderSettingsProvider : IResourceObjectBuilderSettingsProvider { - private readonly IJsonApiOptions _options; - private readonly IAttributeBehaviourService _attributeBehaviour; + private readonly IOmitDefaultService _defaultAttributeValues; + private readonly IOmitNullService _nullAttributeValues; - public ResourceObjectBuilderSettingsProvider(IJsonApiOptions options, IAttributeBehaviourService attributeBehaviour) + public ResourceObjectBuilderSettingsProvider(IOmitDefaultService defaultAttributeValues, + IOmitNullService nullAttributeValues) { - _options = options; - _attributeBehaviour = attributeBehaviour; + _defaultAttributeValues = defaultAttributeValues; + _nullAttributeValues = nullAttributeValues; } /// public ResourceObjectBuilderSettings Get() { - bool omitNullConfig; - if (_attributeBehaviour.OmitNullValuedAttributes.HasValue) - omitNullConfig = _attributeBehaviour.OmitNullValuedAttributes.Value; - else omitNullConfig = _options.NullAttributeResponseBehavior.OmitNullValuedAttributes; - - bool omitDefaultConfig; - if (_attributeBehaviour.OmitDefaultValuedAttributes.HasValue) - omitDefaultConfig = _attributeBehaviour.OmitDefaultValuedAttributes.Value; - else omitDefaultConfig = _options.DefaultAttributeResponseBehavior.OmitDefaultValuedAttributes; - - return new ResourceObjectBuilderSettings(omitNullConfig, omitDefaultConfig); + return new ResourceObjectBuilderSettings(_nullAttributeValues.Config, _defaultAttributeValues.Config); } } } diff --git a/src/JsonApiDotNetCore/Services/IRequestMeta.cs b/src/JsonApiDotNetCore/Services/Contract/IRequestMeta.cs similarity index 100% rename from src/JsonApiDotNetCore/Services/IRequestMeta.cs rename to src/JsonApiDotNetCore/Services/Contract/IRequestMeta.cs diff --git a/src/JsonApiDotNetCore/Services/Contract/IResourceDefinitionProvider.cs b/src/JsonApiDotNetCore/Services/Contract/IResourceDefinitionProvider.cs new file mode 100644 index 0000000000..7f25d22470 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/Contract/IResourceDefinitionProvider.cs @@ -0,0 +1,19 @@ +using System; +using JsonApiDotNetCore.Models; + +namespace JsonApiDotNetCore.Query +{ + /// + /// Retrieves a from the DI container. + /// Abstracts away the creation of the corresponding generic type and usage + /// of the service provider to do so. + /// + public interface IResourceDefinitionProvider + { + /// + /// Retrieves the resource definition associated to . + /// + /// + IResourceDefinition Get(Type resourceType); + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Services/EntityResourceService.cs b/src/JsonApiDotNetCore/Services/EntityResourceService.cs index dbd8b37e5c..cc63ae88b9 100644 --- a/src/JsonApiDotNetCore/Services/EntityResourceService.cs +++ b/src/JsonApiDotNetCore/Services/EntityResourceService.cs @@ -24,11 +24,12 @@ public class EntityResourceService : IResourceService where TResource : class, IIdentifiable { - private readonly IPageQueryService _pageManager; + private readonly IPageService _pageManager; private readonly ICurrentRequest _currentRequest; private readonly IJsonApiOptions _options; - private readonly ITargetedFields _targetedFields; private readonly IResourceGraph _resourceGraph; + private readonly IFilterService _filterService; + private readonly ISortService _sortService; private readonly IEntityRepository _repository; private readonly ILogger _logger; private readonly IResourceHookExecutor _hookExecutor; @@ -37,13 +38,14 @@ public class EntityResourceService : private readonly ContextEntity _currentRequestResource; public EntityResourceService( + ISortService sortService, + IFilterService filterService, IEntityRepository repository, IJsonApiOptions options, - ITargetedFields updatedFields, ICurrentRequest currentRequest, IIncludeService includeService, ISparseFieldsService sparseFieldsService, - IPageQueryService pageManager, + IPageService pageManager, IResourceGraph resourceGraph, IResourceHookExecutor hookExecutor = null, ILoggerFactory loggerFactory = null) @@ -53,8 +55,9 @@ public EntityResourceService( _sparseFieldsService = sparseFieldsService; _pageManager = pageManager; _options = options; - _targetedFields = updatedFields; _resourceGraph = resourceGraph; + _sortService = sortService; + _filterService = filterService; _repository = repository; _hookExecutor = hookExecutor; _logger = loggerFactory?.CreateLogger>(); @@ -103,7 +106,10 @@ public virtual async Task> GetAsync() if (ShouldIncludeRelationships()) entities = IncludeRelationships(entities); - entities = _repository.Select(entities, _currentRequest.QuerySet?.Fields); + + var fields = _sparseFieldsService.Get(); + if (fields.Any()) + entities = _repository.Select(entities, fields); if (!IsNull(_hookExecutor, entities)) { @@ -223,16 +229,11 @@ protected virtual async Task> ApplyPageQueryAsync(IQuerya protected virtual IQueryable ApplySortAndFilterQuery(IQueryable entities) { - var query = _currentRequest.QuerySet; - - if (_currentRequest.QuerySet == null) - return entities; + foreach (var query in _filterService.Get()) + entities = _repository.Filter(entities, query); - if (query.Filters.Count > 0) - foreach (var filter in query.Filters) - entities = _repository.Filter(entities, filter); - - entities = _repository.Sort(entities, query.SortParameters); + foreach (var query in _sortService.Get()) + entities = _repository.Sort(entities, query); return entities; } @@ -258,7 +259,7 @@ protected virtual IQueryable IncludeRelationships(IQueryable GetWithRelationshipsAsync(TId id) { var sparseFieldset = _sparseFieldsService.Get(); - var query = _repository.Select(_repository.Get(), sparseFieldset.Select(a => a.InternalAttributeName).ToList()).Where(e => e.Id.Equals(id)); + var query = _repository.Select(_repository.Get(), sparseFieldset).Where(e => e.Id.Equals(id)); foreach (var chain in _includeService.Get()) query = _repository.Include(query, chain.ToArray()); @@ -279,7 +280,6 @@ private bool ShouldIncludeRelationships() return _includeService.Get().Count() > 0; } - private bool IsNull(params object[] values) { foreach (var val in values) @@ -311,12 +311,12 @@ public class EntityResourceService : EntityResourceService where TResource : class, IIdentifiable { - public EntityResourceService(IEntityRepository repository, IJsonApiOptions options, - ITargetedFields updatedFields, ICurrentRequest currentRequest, + public EntityResourceService(ISortService sortService, IFilterService filterService, IEntityRepository repository, + IJsonApiOptions options, ICurrentRequest currentRequest, IIncludeService includeService, ISparseFieldsService sparseFieldsService, - IPageQueryService pageManager, IResourceGraph resourceGraph, + IPageService pageManager, IResourceGraph resourceGraph, IResourceHookExecutor hookExecutor = null, ILoggerFactory loggerFactory = null) - : base(repository, options, updatedFields, currentRequest, includeService, sparseFieldsService, pageManager, resourceGraph, hookExecutor, loggerFactory) + : base(sortService, filterService, repository, options, currentRequest, includeService, sparseFieldsService, pageManager, resourceGraph, hookExecutor, loggerFactory) { } } diff --git a/src/JsonApiDotNetCore/Services/QueryAccessor.cs b/src/JsonApiDotNetCore/Services/QueryAccessor.cs deleted file mode 100644 index 434179080c..0000000000 --- a/src/JsonApiDotNetCore/Services/QueryAccessor.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using System.Linq; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Managers.Contracts; -using Microsoft.Extensions.Logging; - -namespace JsonApiDotNetCore.Services -{ - public interface IQueryAccessor - { - bool TryGetValue(string key, out T value); - - /// - /// Gets the query value and throws a if it is not present. - /// If the exception is not caught, the middleware will return an HTTP 422 response. - /// - /// - T GetRequired(string key); - } - - /// - /// Accessing queries - /// - public class QueryAccessor : IQueryAccessor - { - private readonly ICurrentRequest _currentRequest; - private readonly ILogger _logger; - - /// - /// Creates an instance which can be used to access the qury - /// - /// - /// - public QueryAccessor( - ICurrentRequest currentRequest, - ILogger logger) - { - _currentRequest = currentRequest; - _logger = logger; - } - - - public T GetRequired(string key) - { - if (TryGetValue(key, out T result) == false) - throw new JsonApiException(422, $"'{key}' is not a valid '{typeof(T).Name}' value for query parameter {key}"); - - return result; - } - - public bool TryGetValue(string key, out T value) - { - value = default(T); - - var stringValue = GetFilterValue(key); - if (stringValue == null) - { - if (_logger.IsEnabled(LogLevel.Information)) - { - _logger.LogInformation($"'{key}' was not found in the query collection"); - } - - return false; - } - - try - { - value = TypeHelper.ConvertType(stringValue); - return true; - } - catch (FormatException) - { - if (_logger.IsEnabled(LogLevel.Information)) - { - _logger.LogInformation( - $"'{value}' is not a valid '{typeof(T).Name}' value for query parameter {key}"); - } - - return false; - } - } - - private string GetFilterValue(string key) { - var publicValue = _currentRequest.QuerySet.Filters - .FirstOrDefault(f => string.Equals(f.Attribute, key, StringComparison.OrdinalIgnoreCase))?.Value; - - if(publicValue != null) - return publicValue; - - var internalValue = _currentRequest.QuerySet.Filters - .FirstOrDefault(f => string.Equals(f.Attribute, key, StringComparison.OrdinalIgnoreCase))?.Value; - - if(internalValue != null) { - _logger.LogWarning("Locating filters by the internal propterty name is deprecated. You should use the public attribute name instead."); - return publicValue; - } - - return null; - } - } -} diff --git a/src/JsonApiDotNetCore/Services/QueryComposer.cs b/src/JsonApiDotNetCore/Services/QueryComposer.cs deleted file mode 100644 index 713a423d81..0000000000 --- a/src/JsonApiDotNetCore/Services/QueryComposer.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Internal.Query; -using JsonApiDotNetCore.Managers.Contracts; - -namespace JsonApiDotNetCore.Services -{ - public interface IQueryComposer - { - string Compose(ICurrentRequest jsonApiContext); - } - - public class QueryComposer : IQueryComposer - { - public string Compose(ICurrentRequest currentRequest) - { - string result = ""; - if (currentRequest != null && currentRequest.QuerySet != null) - { - List filterQueries = currentRequest.QuerySet.Filters; - if (filterQueries.Count > 0) - { - foreach (FilterQuery filter in filterQueries) - { - result += ComposeSingleFilter(filter); - } - } - } - return result; - } - - private string ComposeSingleFilter(FilterQuery query) - { - var result = "&filter"; - var operation = string.IsNullOrWhiteSpace(query.Operation) ? query.Operation : query.Operation + ":"; - result += QueryConstants.OPEN_BRACKET + query.Attribute + QueryConstants.CLOSE_BRACKET + "=" + operation + query.Value; - return result; - } - } -} diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs deleted file mode 100644 index 1b8e19f8f0..0000000000 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ /dev/null @@ -1,234 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Contracts; -using JsonApiDotNetCore.Internal.Query; -using JsonApiDotNetCore.Managers.Contracts; -using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Query; -using Microsoft.AspNetCore.Http; - -namespace JsonApiDotNetCore.Services -{ - - public interface IQueryParser - { - void Parse(IQueryCollection query); - } - - public class QueryParser : IQueryParser - { - private readonly IncludeService _includeService; - private readonly SparseFieldsService _sparseFieldsService; - private readonly FilterService _filterService; - private readonly SortService _sortService; - private readonly OmitDefaultService _omitDefaultService; - private readonly OmitNullService _omitNull; - private readonly PageService _pageService; - - private readonly ICurrentRequest _currentRequest; - private readonly IContextEntityProvider _provider; - private readonly IJsonApiOptions _options; - private readonly IServiceProvider _sp; - private ContextEntity _primaryResource; - - public QueryParser( - ICurrentRequest currentRequest, - IContextEntityProvider provider, - IJsonApiOptions options) - { - _currentRequest = currentRequest; - _provider = provider; - _options = options; - } - - public virtual void Parse(IQueryCollection query) - { - - _primaryResource = _currentRequest.GetRequestResource(); - var disabledQueries = _currentRequest.DisabledQueryParams; - - foreach (var pair in query) - { - if (pair.Key.StartsWith(QueryConstants.FILTER, StringComparison.Ordinal)) - { - if (disabledQueries.HasFlag(QueryParams.Filters) == false) - //querySet.Filters.AddRange(ParseFilterQuery(pair.Key, pair.Value)); - continue; - } - - if (pair.Key.StartsWith(QueryConstants.SORT, StringComparison.Ordinal)) - { - if (disabledQueries.HasFlag(QueryParams.Sort) == false) - //querySet.SortParameters = ParseSortParameters(pair.Value); - continue; - } - - if (pair.Key.StartsWith(_includeService.Name, StringComparison.Ordinal)) - { - if (disabledQueries.HasFlag(QueryParams.Include) == false) - _includeService.Parse(null, pair.Value); - continue; - } - - if (pair.Key.StartsWith(QueryConstants.PAGE, StringComparison.Ordinal)) - { - if (disabledQueries.HasFlag(QueryParams.Page) == false) - //querySet.PageQuery = ParsePageQuery(querySet.PageQuery, pair.Key, pair.Value); - continue; - } - - if (pair.Key.StartsWith(QueryConstants.FIELDS, StringComparison.Ordinal)) - { - if (disabledQueries.HasFlag(QueryParams.Fields) == false) - _sparseFieldsService.Parse(pair.Key, pair.Value); - continue; - } - - if (_options.AllowCustomQueryParameters == false) - throw new JsonApiException(400, $"{pair} is not a valid query."); - } - } - - private void GetQueryParameterServices() - { - var type = typeof(IQueryParameterService); - var types = AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(a => a.GetTypes()) - .Where(t => t.IsInterface && t.Inherits(type)) - .Select(t => (IQueryParameterService)_sp.GetService(t)); - } - - protected virtual List ParseFilterQuery(string key, string value) - { - // expected input = filter[id]=1 - // expected input = filter[id]=eq:1 - var queries = new List(); - var propertyName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; - - // InArray case - string op = GetFilterOperation(value); - if (string.Equals(op, FilterOperations.@in.ToString(), StringComparison.OrdinalIgnoreCase) - || string.Equals(op, FilterOperations.nin.ToString(), StringComparison.OrdinalIgnoreCase)) - { - (var operation, var filterValue) = ParseFilterOperation(value); - queries.Add(new FilterQuery(propertyName, filterValue, op)); - } - else - { - var values = value.Split(QueryConstants.COMMA); - foreach (var val in values) - { - (var operation, var filterValue) = ParseFilterOperation(val); - queries.Add(new FilterQuery(propertyName, filterValue, operation)); - } - } - - return queries; - } - - protected virtual (string operation, string value) ParseFilterOperation(string value) - { - if (value.Length < 3) - return (string.Empty, value); - - var operation = GetFilterOperation(value); - var values = value.Split(QueryConstants.COLON); - - if (string.IsNullOrEmpty(operation)) - return (string.Empty, value); - - value = string.Join(QueryConstants.COLON_STR, values.Skip(1)); - - return (operation, value); - } - - protected virtual PageQuery ParsePageQuery(PageQuery pageQuery, string key, string value) - { - // expected input = page[size]=10 - // page[number]=1 - pageQuery = pageQuery ?? new PageQuery(); - - var propertyName = key.Split(QueryConstants.OPEN_BRACKET, QueryConstants.CLOSE_BRACKET)[1]; - - const string SIZE = "size"; - const string NUMBER = "number"; - - if (propertyName == SIZE) - { - pageQuery.PageSize = int.TryParse(value, out var pageSize) ? - pageSize : - throw new JsonApiException(400, $"Invalid page size '{value}'"); - } - - else if (propertyName == NUMBER) - pageQuery.PageOffset = int.TryParse(value, out var pageOffset) ? - pageOffset : - throw new JsonApiException(400, $"Invalid page size '{value}'"); - - return pageQuery; - } - - // sort=id,name - // sort=-id - protected virtual List ParseSortParameters(string value) - { - var sortParameters = new List(); - - const char DESCENDING_SORT_OPERATOR = '-'; - var sortSegments = value.Split(QueryConstants.COMMA); - if (sortSegments.Where(s => s == string.Empty).Count() > 0) - { - throw new JsonApiException(400, "The sort URI segment contained a null value."); - } - foreach (var sortSegment in sortSegments) - { - var propertyName = sortSegment; - var direction = SortDirection.Ascending; - - if (sortSegment[0] == DESCENDING_SORT_OPERATOR) - { - direction = SortDirection.Descending; - propertyName = propertyName.Substring(1); - } - - sortParameters.Add(new SortQuery(direction, propertyName)); - }; - - return sortParameters; - } - - protected virtual AttrAttribute GetAttribute(string propertyName) - { - try - { - return _primaryResource - .Attributes - .Single(attr => attr.Is(propertyName)); - } - catch (InvalidOperationException e) - { - throw new JsonApiException(400, $"Attribute '{propertyName}' does not exist on resource '{_primaryResource.EntityName}'", e); - } - } - - private string GetFilterOperation(string value) - { - var values = value.Split(QueryConstants.COLON); - - if (values.Length == 1) - return string.Empty; - - var operation = values[0]; - // remove prefix from value - if (Enum.TryParse(operation, out FilterOperations op) == false) - return string.Empty; - - return operation; - } - } -} diff --git a/src/JsonApiDotNetCore/Services/ResourceDefinitionProvider.cs b/src/JsonApiDotNetCore/Services/ResourceDefinitionProvider.cs new file mode 100644 index 0000000000..61234125f5 --- /dev/null +++ b/src/JsonApiDotNetCore/Services/ResourceDefinitionProvider.cs @@ -0,0 +1,26 @@ +using System; +using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Services; + +namespace JsonApiDotNetCore.Query +{ + /// + internal class ResourceDefinitionProvider : IResourceDefinitionProvider + { + private readonly IContextEntityProvider _resourceContextProvider; + private readonly IScopedServiceProvider _serviceProvider; + + public ResourceDefinitionProvider(IContextEntityProvider resourceContextProvider, IScopedServiceProvider serviceProvider) + { + _resourceContextProvider = resourceContextProvider; + _serviceProvider = serviceProvider; + } + + /// + public IResourceDefinition Get(Type resourceType) + { + return (IResourceDefinition)_serviceProvider.GetService(_resourceContextProvider.GetContextEntity(resourceType).ResourceType); + } + } +} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs index 5d6bee765c..0f2026e07c 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/NullValuedAttributeHandlingTests.cs @@ -81,7 +81,7 @@ public async Task CheckNullBehaviorCombination(bool? omitNullValuedAttributes, b var httpMethod = new HttpMethod("GET"); var queryString = allowClientOverride.HasValue - ? $"&omitNullValuedAttributes={clientOverride}" + ? $"&omitNull={clientOverride}" : ""; var route = $"/api/v1/todo-items/{_todoItem.Id}?include=owner{queryString}"; var request = new HttpRequestMessage(httpMethod, route); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs index b1aff8882f..3ae12cfdcb 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs @@ -119,11 +119,11 @@ public async Task Can_Filter_On_Not_Equal_Values() // act var response = await _fixture.Client.SendAsync(request); var body = await response.Content.ReadAsStringAsync(); - var list = _fixture.GetDeserializer().DeserializeList(body).Data.First(); + var list = _fixture.GetDeserializer().DeserializeList(body).Data; // assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); - //Assert.DoesNotContain(deserializedTodoItems, x => x.Ordinal == todoItem.Ordinal); + Assert.DoesNotContain(list, x => x.Ordinal == todoItem.Ordinal); } [Fact] diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs index 7ba16c920e..feba7e7ce5 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/SparseFieldSetTests.cs @@ -21,6 +21,7 @@ using JsonApiDotNetCore.Serialization.Client; using JsonApiDotNetCore.Builders; using JsonApiDotNetCoreExampleTests.Helpers.Models; +using JsonApiDotNetCore.Services; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { @@ -29,6 +30,7 @@ public class SparseFieldSetTests { private TestFixture _fixture; private readonly AppDbContext _dbContext; + private IFieldsExplorer _explorer; private Faker _personFaker; private Faker _todoItemFaker; @@ -36,6 +38,7 @@ public SparseFieldSetTests(TestFixture fixture) { _fixture = fixture; _dbContext = fixture.GetService(); + _explorer = fixture.GetService(); _personFaker = new Faker() .RuleFor(p => p.FirstName, f => f.Name.FirstName()) .RuleFor(p => p.LastName, f => f.Name.LastName()) @@ -69,7 +72,7 @@ public async Task Can_Select_Sparse_Fieldsets() var query = _dbContext .TodoItems .Where(t => t.Id == todoItem.Id) - .Select(fields); + .Select(_explorer.GetAttributes(e => new { e.Id, e.Description, e.CreatedDate, e.AchievedDate } ).ToList()); var resultSql = StringExtensions.Normalize(query.ToSql()); var result = await query.FirstAsync(); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs index cd0eb17c04..270ae51b53 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs @@ -190,6 +190,7 @@ public async Task Can_Filter_TodoItems_ByParent_Using_IsNotNull_Operator() var otherTodoItem = _todoItemFaker.Generate(); otherTodoItem.Assignee = null; + _context.RemoveRange(_context.TodoItems); _context.TodoItems.AddRange(new[] { todoItem, otherTodoItem }); _context.SaveChanges(); @@ -203,11 +204,10 @@ public async Task Can_Filter_TodoItems_ByParent_Using_IsNotNull_Operator() Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadAsStringAsync(); - var todoItems = _fixture.GetDeserializer().DeserializeList(body).Data; + var list = _fixture.GetDeserializer().DeserializeList(body).Data; // Assert - Assert.NotEmpty(todoItems); - Assert.All(todoItems, t => Assert.NotNull(t.Assignee)); + Assert.Equal(todoItem.Id, list.Single().Id); } [Fact] diff --git a/test/UnitTests/Builders/LinkBuilderTests.cs b/test/UnitTests/Builders/LinkBuilderTests.cs index 51b712e4ae..193b99f1f2 100644 --- a/test/UnitTests/Builders/LinkBuilderTests.cs +++ b/test/UnitTests/Builders/LinkBuilderTests.cs @@ -15,7 +15,7 @@ namespace UnitTests { public class LinkBuilderTests { - private readonly IPageQueryService _pageManager; + private readonly IPageService _pageManager; private readonly Mock _provider = new Mock(); private const string _host = "http://www.example.com"; private const string _topSelf = "http://www.example.com/articles"; @@ -189,9 +189,9 @@ private ILinksConfiguration GetConfiguration(Link resourceLinks = Link.All, return config.Object; } - private IPageQueryService GetPageManager() + private IPageService GetPageManager() { - var mock = new Mock(); + var mock = new Mock(); mock.Setup(m => m.ShouldPaginate()).Returns(true); mock.Setup(m => m.CurrentPage).Returns(2); mock.Setup(m => m.TotalPages).Returns(3); diff --git a/test/UnitTests/QueryParameters/FilterServiceTests.cs b/test/UnitTests/QueryParameters/FilterServiceTests.cs new file mode 100644 index 0000000000..ab3b5d13ae --- /dev/null +++ b/test/UnitTests/QueryParameters/FilterServiceTests.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal.Query; +using JsonApiDotNetCore.Query; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace UnitTests.QueryParameters +{ + public class FilterServiceTests : QueryParametersUnitTestCollection + { + public FilterService GetService() + { + return new FilterService(MockResourceDefinitionProvider(), _graph, MockCurrentRequest(_articleResourceContext)); + } + + [Fact] + public void Name_FilterService_IsCorrect() + { + // arrange + var filterService = GetService(); + + // act + var name = filterService.Name; + + // assert + Assert.Equal("filter", name); + } + + [Theory] + [InlineData("title", "", "value")] + [InlineData("title", "eq:", "value")] + [InlineData("title", "lt:", "value")] + [InlineData("title", "gt:", "value")] + [InlineData("title", "le:", "value")] + [InlineData("title", "ge:", "value")] + [InlineData("title", "like:", "value")] + [InlineData("title", "ne:", "value")] + [InlineData("title", "in:", "value")] + [InlineData("title", "nin:", "value")] + [InlineData("title", "isnull:", "")] + [InlineData("title", "isnotnull:", "")] + [InlineData("title", "", "2017-08-15T22:43:47.0156350-05:00")] + [InlineData("title", "le:", "2017-08-15T22:43:47.0156350-05:00")] + public void Parse_ValidFilters_CanParse(string key, string @operator, string value) + { + // arrange + var queryValue = @operator + value; + var query = new KeyValuePair($"filter[{key}]", new StringValues(queryValue)); + var filterService = GetService(); + + // act + filterService.Parse(query); + var filter = filterService.Get().Single(); + + // assert + if (!string.IsNullOrEmpty(@operator)) + Assert.Equal(@operator.Replace(":", ""), filter.Operation.ToString("G")); + else + Assert.Equal(FilterOperation.eq, filter.Operation); + + if (!string.IsNullOrEmpty(value)) + Assert.Equal(value, filter.Value); + } + } +} diff --git a/test/UnitTests/QueryParameters/IncludeServiceTests.cs b/test/UnitTests/QueryParameters/IncludeServiceTests.cs new file mode 100644 index 0000000000..8ed7b0c034 --- /dev/null +++ b/test/UnitTests/QueryParameters/IncludeServiceTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Query; +using Microsoft.Extensions.Primitives; +using UnitTests.TestModels; +using Xunit; + +namespace UnitTests.QueryParameters +{ + public class IncludeServiceTests : QueryParametersUnitTestCollection + { + + public IncludeService GetService(ContextEntity resourceContext = null) + { + return new IncludeService(_graph, MockCurrentRequest(resourceContext ?? _articleResourceContext)); + } + + [Fact] + public void Name_IncludeService_IsCorrect() + { + // arrange + var filterService = GetService(); + + // act + var name = filterService.Name; + + // assert + Assert.Equal("include", name); + } + + [Fact] + public void Parse_MultipleNestedChains_CanParse() + { + // arrange + const string chain = "author.blogs.reviewer.favorite-food,reviewer.blogs.author.favorite-song"; + var query = new KeyValuePair("include", new StringValues(chain)); + var service = GetService(); + + // act + service.Parse(query); + + // assert + var chains = service.Get(); + Assert.Equal(2, chains.Count); + var firstChain = chains[0]; + Assert.Equal("author", firstChain.First().PublicRelationshipName); + Assert.Equal("favorite-food", firstChain.Last().PublicRelationshipName); + var secondChain = chains[1]; + Assert.Equal("reviewer", secondChain.First().PublicRelationshipName); + Assert.Equal("favorite-song", secondChain.Last().PublicRelationshipName); + } + + [Fact] + public void Parse_ChainsOnWrongMainResource_ThrowsJsonApiException() + { + // arrange + const string chain = "author.blogs.reviewer.favorite-food,reviewer.blogs.author.favorite-song"; + var query = new KeyValuePair("include", new StringValues(chain)); + var service = GetService(_graph.GetContextEntity()); + + // act, assert + var exception = Assert.Throws( () => service.Parse(query)); + Assert.Contains("Invalid", exception.Message); + } + + [Fact] + public void Parse_NotIncludable_ThrowsJsonApiException() + { + // arrange + const string chain = "cannot-include"; + var query = new KeyValuePair("include", new StringValues(chain)); + var service = GetService(); + + // act, assert + var exception = Assert.Throws(() => service.Parse(query)); + Assert.Contains("not allowed", exception.Message); + } + + [Fact] + public void Parse_NonExistingRelationship_ThrowsJsonApiException() + { + // arrange + const string chain = "nonsense"; + var query = new KeyValuePair("include", new StringValues(chain)); + var service = GetService(); + + // act, assert + var exception = Assert.Throws(() => service.Parse(query)); + Assert.Contains("Invalid", exception.Message); + } + + [Fact] + public void Parse_EmptyChain_ThrowsJsonApiException() + { + // arrange + const string chain = ""; + var query = new KeyValuePair("include", new StringValues(chain)); + var service = GetService(); + + // act, assert + var exception = Assert.Throws(() => service.Parse(query)); + Assert.Contains("Include parameter must not be empty if provided", exception.Message); + } + } +} diff --git a/test/UnitTests/QueryParameters/IncludedServiceTests.cs b/test/UnitTests/QueryParameters/IncludedServiceTests.cs deleted file mode 100644 index 57e93d3869..0000000000 --- a/test/UnitTests/QueryParameters/IncludedServiceTests.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Linq; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Query; -using Xunit; - -namespace UnitTests.QueryParameters -{ - public class IncludedServiceTests : QueryParametersUnitTestCollection - { - - public IncludeService GetService(ContextEntity resourceContext = null) - { - return new IncludeService(resourceContext ?? _articleResourceContext , _graph); - } - - [Fact] - public void Parse_ShortChain_CanParse() - { - // arrange - const string chain = "author"; - - var service = GetService(); - - // act - service.Parse(null, "author"); - - // assert - var chains = service.Get(); - Assert.Equal(1, chains.Count); - var relationship = chains.First().First(); - Assert.Equal(chain, relationship.PublicRelationshipName); - } - - } -} diff --git a/test/UnitTests/QueryParameters/OmitDefaultService.cs b/test/UnitTests/QueryParameters/OmitDefaultService.cs new file mode 100644 index 0000000000..e4e64d58b5 --- /dev/null +++ b/test/UnitTests/QueryParameters/OmitDefaultService.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Query; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace UnitTests.QueryParameters +{ + public class OmitDefaultServiceTests : QueryParametersUnitTestCollection + { + public OmitDefaultService GetService(bool @default, bool @override) + { + var options = new JsonApiOptions + { + DefaultAttributeResponseBehavior = new DefaultAttributeResponseBehavior(@default, @override) + }; + + return new OmitDefaultService(options); + } + + [Fact] + public void Name_OmitNullService_IsCorrect() + { + // arrange + var service = GetService(true, true); + + // act + var name = service.Name; + + // assert + Assert.Equal("omitdefault", name); + } + + [Theory] + [InlineData("false", true, true, false)] + [InlineData("false", true, false, true)] + [InlineData("true", false, true, true)] + [InlineData("true", false, false, false)] + public void Parse_QueryConfigWithApiSettings_CanParse(string queryConfig, bool @default, bool @override, bool expected) + { + // arrange + var query = new KeyValuePair($"omitNull", new StringValues(queryConfig)); + var service = GetService(@default, @override); + + // act + service.Parse(query); + + // assert + Assert.Equal(expected, service.Config); + } + } +} diff --git a/test/UnitTests/QueryParameters/OmitNullService.cs b/test/UnitTests/QueryParameters/OmitNullService.cs new file mode 100644 index 0000000000..f9f8237e50 --- /dev/null +++ b/test/UnitTests/QueryParameters/OmitNullService.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Query; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace UnitTests.QueryParameters +{ + public class OmitNullServiceTests : QueryParametersUnitTestCollection + { + public OmitNullService GetService(bool @default, bool @override) + { + var options = new JsonApiOptions + { + NullAttributeResponseBehavior = new NullAttributeResponseBehavior(@default, @override) + }; + + return new OmitNullService(options); + } + + [Fact] + public void Name_OmitNullService_IsCorrect() + { + // arrange + var service = GetService(true, true); + + // act + var name = service.Name; + + // assert + Assert.Equal("omitnull", name); + } + + [Theory] + [InlineData("false", true, true, false)] + [InlineData("false", true, false, true)] + [InlineData("true", false, true, true)] + [InlineData("true", false, false, false)] + public void Parse_QueryConfigWithApiSettings_CanParse(string queryConfig, bool @default, bool @override, bool expected) + { + // arrange + var query = new KeyValuePair($"omitNull", new StringValues(queryConfig)); + var service = GetService(@default, @override); + + // act + service.Parse(query); + + // assert + Assert.Equal(expected, service.Config); + } + } +} diff --git a/test/UnitTests/QueryParameters/PageServiceTests.cs b/test/UnitTests/QueryParameters/PageServiceTests.cs new file mode 100644 index 0000000000..003efbe195 --- /dev/null +++ b/test/UnitTests/QueryParameters/PageServiceTests.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Query; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace UnitTests.QueryParameters +{ + public class PageServiceTests : QueryParametersUnitTestCollection + { + public PageService GetService() + { + return new PageService(new JsonApiOptions()); + } + + [Fact] + public void Name_PageService_IsCorrect() + { + // arrange + var filterService = GetService(); + + // act + var name = filterService.Name; + + // assert + Assert.Equal("page", name); + } + + [Theory] + [InlineData("1", 1, false)] + [InlineData("abcde", 0, true)] + [InlineData("", 0, true)] + public void Parse_PageSize_CanParse(string value, int expectedValue, bool shouldThrow) + { + // arrange + var query = new KeyValuePair($"page[size]", new StringValues(value)); + var service = GetService(); + + // act + if (shouldThrow) + { + var ex = Assert.Throws(() => service.Parse(query)); + Assert.Equal(400, ex.GetStatusCode()); + } + else + { + service.Parse(query); + Assert.Equal(expectedValue, service.PageSize); + } + } + + [Theory] + [InlineData("1", 1, false)] + [InlineData("abcde", 0, true)] + [InlineData("", 0, true)] + public void Parse_PageNumber_CanParse(string value, int expectedValue, bool shouldThrow) + { + // arrange + var query = new KeyValuePair($"page[number]", new StringValues(value)); + var service = GetService(); + + + // act + if (shouldThrow) + { + var ex = Assert.Throws(() => service.Parse(query)); + Assert.Equal(400, ex.GetStatusCode()); + } + else + { + service.Parse(query); + Assert.Equal(expectedValue, service.CurrentPage); + } + } + } +} diff --git a/test/UnitTests/QueryParameters/QueryParametersUnitTestCollection.cs b/test/UnitTests/QueryParameters/QueryParametersUnitTestCollection.cs index 36835634cb..9cc6d80fc4 100644 --- a/test/UnitTests/QueryParameters/QueryParametersUnitTestCollection.cs +++ b/test/UnitTests/QueryParameters/QueryParametersUnitTestCollection.cs @@ -1,6 +1,11 @@ -using JsonApiDotNetCore.Builders; +using System; +using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; +using JsonApiDotNetCore.Managers.Contracts; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Query; +using Moq; using UnitTests.TestModels; namespace UnitTests.QueryParameters @@ -21,5 +26,25 @@ public QueryParametersUnitTestCollection() _graph = builder.Build(); _articleResourceContext = _graph.GetContextEntity
(); } + + public ICurrentRequest MockCurrentRequest(ContextEntity requestResource = null) + { + var mock = new Mock(); + + if (requestResource != null) + mock.Setup(m => m.GetRequestResource()).Returns(requestResource); + + return mock.Object; + } + + public IResourceDefinitionProvider MockResourceDefinitionProvider(params (Type, IResourceDefinition)[] rds) + { + var mock = new Mock(); + + foreach (var (type, resourceDefinition) in rds) + mock.Setup(m => m.Get(type)).Returns(resourceDefinition); + + return mock.Object; + } } } \ No newline at end of file diff --git a/test/UnitTests/QueryParameters/SortServiceTests.cs b/test/UnitTests/QueryParameters/SortServiceTests.cs new file mode 100644 index 0000000000..1ca38d192e --- /dev/null +++ b/test/UnitTests/QueryParameters/SortServiceTests.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Query; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace UnitTests.QueryParameters +{ + public class SortServiceTests : QueryParametersUnitTestCollection + { + public SortService GetService() + { + return new SortService(MockResourceDefinitionProvider(), _graph, MockCurrentRequest(_articleResourceContext)); + } + + [Fact] + public void Name_SortService_IsCorrect() + { + // arrange + var filterService = GetService(); + + // act + var name = filterService.Name; + + // assert + Assert.Equal("sort", name); + } + + [Theory] + [InlineData("text,,1")] + [InlineData("text,hello,,5")] + [InlineData(",,2")] + public void Parse_InvalidSortQuery_ThrowsJsonApiException(string stringSortQuery) + { + // arrange + var query = new KeyValuePair($"sort", stringSortQuery); + var sortService = GetService(); + + // act, assert + var exception = Assert.Throws(() => sortService.Parse(query)); + Assert.Contains("sort", exception.Message); + } + } +} diff --git a/test/UnitTests/QueryParameters/SparseFieldsServiceTests.cs b/test/UnitTests/QueryParameters/SparseFieldsServiceTests.cs new file mode 100644 index 0000000000..a22da83021 --- /dev/null +++ b/test/UnitTests/QueryParameters/SparseFieldsServiceTests.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Linq; +using JsonApiDotNetCore.Internal; +using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Query; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace UnitTests.QueryParameters +{ + public class SparseFieldsServiceTests : QueryParametersUnitTestCollection + { + public SparseFieldsService GetService(ContextEntity contextEntity = null) + { + return new SparseFieldsService(_graph, MockCurrentRequest(contextEntity ?? _articleResourceContext)); + } + + [Fact] + public void Name_SparseFieldsService_IsCorrect() + { + // arrange + var filterService = GetService(); + + // act + var name = filterService.Name; + + // assert + Assert.Equal("fields", name); + } + + [Fact] + public void Parse_ValidSelection_CanParse() + { + // arrange + const string type = "articles"; + const string attrName = "some-field"; + const string internalAttrName = "SomeField"; + var attribute = new AttrAttribute(attrName) { InternalAttributeName = internalAttrName }; + + var query = new KeyValuePair($"fields[{type}]", new StringValues(attrName)); + + var contextEntity = new ContextEntity + { + EntityName = type, + Attributes = new List { attribute }, + Relationships = new List() + }; + var service = GetService(contextEntity); + + // act + service.Parse(query); + var result = service.Get(); + + // assert + Assert.NotEmpty(result); + Assert.Equal(attribute, result.Single()); + } + + [Fact] + public void Parse_InvalidField_ThrowsJsonApiException() + { + // arrange + const string type = "articles"; + const string attrName = "dne"; + + var query = new KeyValuePair($"fields[{type}]", new StringValues(attrName)); + + var contextEntity = new ContextEntity + { + EntityName = type, + Attributes = new List(), + Relationships = new List() + }; + + var service = GetService(contextEntity); + + // act , assert + var ex = Assert.Throws(() => service.Parse(query)); + Assert.Equal(400, ex.GetStatusCode()); + } + } +} diff --git a/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs b/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs index fa75a29a2c..7218c5ff50 100644 --- a/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs +++ b/test/UnitTests/Serialization/Server/ResponseSerializerTests.cs @@ -7,7 +7,6 @@ using Newtonsoft.Json; using Xunit; using UnitTests.TestModels; -using Person = UnitTests.TestModels.Person; namespace UnitTests.Serialization.Server { diff --git a/test/UnitTests/Services/EntityResourceService_Tests.cs b/test/UnitTests/Services/EntityResourceService_Tests.cs index 0af41b61e6..2a55ec8e37 100644 --- a/test/UnitTests/Services/EntityResourceService_Tests.cs +++ b/test/UnitTests/Services/EntityResourceService_Tests.cs @@ -21,14 +21,14 @@ public class EntityResourceService_Tests private readonly Mock> _repositoryMock = new Mock>(); private readonly ILoggerFactory _loggerFactory = new Mock().Object; private readonly Mock _crMock; - private readonly Mock _pgsMock; + private readonly Mock _pgsMock; private readonly Mock _ufMock; private readonly IResourceGraph _resourceGraph; public EntityResourceService_Tests() { _crMock = new Mock(); - _pgsMock = new Mock(); + _pgsMock = new Mock(); _ufMock = new Mock(); _resourceGraph = new ResourceGraphBuilder() .AddResource() @@ -45,7 +45,7 @@ public async Task GetRelationshipAsync_Passes_Public_ResourceName_To_Repository( const string relationshipName = "collection"; var relationship = new HasOneAttribute(relationshipName); - _repositoryMock.Setup(m => m.GetAndIncludeAsync(id, relationship)) + _repositoryMock.Setup(m => m.GetAndIncludeAsync(id, relationship, null)) .ReturnsAsync(new TodoItem()); var service = GetService(); @@ -54,7 +54,7 @@ public async Task GetRelationshipAsync_Passes_Public_ResourceName_To_Repository( await service.GetRelationshipAsync(id, relationshipName); // assert - _repositoryMock.Verify(m => m.GetAndIncludeAsync(id, relationship), Times.Once); + _repositoryMock.Verify(m => m.GetAndIncludeAsync(id, relationship, null), Times.Once); } [Fact] @@ -70,7 +70,7 @@ public async Task GetRelationshipAsync_Returns_Relationship_Value() Collection = new TodoItemCollection { Id = Guid.NewGuid() } }; - _repositoryMock.Setup(m => m.GetAndIncludeAsync(id, relationship)) + _repositoryMock.Setup(m => m.GetAndIncludeAsync(id, relationship, null)) .ReturnsAsync(todoItem); var repository = GetService(); @@ -86,7 +86,7 @@ public async Task GetRelationshipAsync_Returns_Relationship_Value() private EntityResourceService GetService() { - return new EntityResourceService(_repositoryMock.Object, new JsonApiOptions(), _ufMock.Object, _crMock.Object, null, null, _pgsMock.Object, _resourceGraph); + return new EntityResourceService(null, null, _repositoryMock.Object, new JsonApiOptions(), _crMock.Object, null, null, _pgsMock.Object, _resourceGraph); } } } diff --git a/test/UnitTests/Services/QueryAccessorTests.cs b/test/UnitTests/Services/QueryAccessorTests.cs deleted file mode 100644 index d743ad58f7..0000000000 --- a/test/UnitTests/Services/QueryAccessorTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Collections.Generic; -using JsonApiDotNetCore.Internal; -using JsonApiDotNetCore.Internal.Query; -using JsonApiDotNetCore.Managers.Contracts; -using JsonApiDotNetCore.Services; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit; - -namespace UnitTests.Services -{ - public class QueryAccessorTests - { - private readonly Mock _rmMock; - private readonly Mock> _loggerMock; - private readonly Mock _queryMock; - - public QueryAccessorTests() - { - _rmMock = new Mock(); - _loggerMock = new Mock>(); - _queryMock = new Mock(); - } - - [Fact] - public void GetGuid_GuidIsValid_IsReturnedCorrectly() - { - // Arrange - const string key = "SomeId"; - var value = Guid.NewGuid(); - var querySet = new QuerySet - { - Filters = new List { - new FilterQuery(key, value.ToString(), "eq") - } - }; - - _rmMock.Setup(c => c.QuerySet).Returns(querySet); - - var service = new QueryAccessor(_rmMock.Object, _loggerMock.Object); - - // act - var success = service.TryGetValue("SomeId", out Guid result); - - // assert - Assert.True(success); - Assert.Equal(value, result); - } - - [Fact] - public void GetRequired_Throws_If_Not_Present() - { - // Arrange - const string key = "SomeId"; - var value = Guid.NewGuid(); - - var querySet = new QuerySet - { - Filters = new List { - new FilterQuery(key, value.ToString(), "eq") - } - }; - - _rmMock.Setup(c => c.QuerySet).Returns(querySet); - - var service = new QueryAccessor(_rmMock.Object, _loggerMock.Object); - - // act - var exception = Assert.Throws(() => service.GetRequired("Invalid")); - - // assert - Assert.Equal(422, exception.GetStatusCode()); - } - - [Fact] - public void GetRequired_Does_Not_Throw_If_Present() - { - // arrange - const string key = "SomeId"; - var value = Guid.NewGuid(); - - var querySet = new QuerySet - { - Filters = new List { - new FilterQuery(key, value.ToString(), "eq") - } - }; - - _rmMock.Setup(c => c.QuerySet).Returns(querySet); - - var service = new QueryAccessor(_rmMock.Object, _loggerMock.Object); - - // Act - var result = service.GetRequired("SomeId"); - - // Assert - Assert.Equal(value, result); - } - } -} diff --git a/test/UnitTests/Services/QueryComposerTests.cs b/test/UnitTests/Services/QueryComposerTests.cs deleted file mode 100644 index 817b3810e3..0000000000 --- a/test/UnitTests/Services/QueryComposerTests.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Collections.Generic; -using JsonApiDotNetCore.Internal.Query; -using JsonApiDotNetCore.Managers.Contracts; -using JsonApiDotNetCore.Services; -using Moq; -using Xunit; - -namespace UnitTests.Services -{ - public class QueryComposerTests - { - - [Fact] - public void Can_ComposeEqual_FilterStringForUrl() - { - // arrange - var filter = new FilterQuery("attribute", "value", "eq"); - var querySet = new QuerySet(); - List filters = new List(); - filters.Add(filter); - querySet.Filters = filters; - - var rmMock = new Mock(); - rmMock - .Setup(m => m.QuerySet) - .Returns(querySet); - - var queryComposer = new QueryComposer(); - // act - var filterString = queryComposer.Compose(rmMock.Object); - // assert - Assert.Equal("&filter[attribute]=eq:value", filterString); - } - - [Fact] - public void Can_ComposeLessThan_FilterStringForUrl() - { - // arrange - var filter = new FilterQuery("attribute", "value", "le"); - var filter2 = new FilterQuery("attribute2", "value2", ""); - var querySet = new QuerySet(); - List filters = new List(); - filters.Add(filter); - filters.Add(filter2); - querySet.Filters = filters; - var rmMock = new Mock(); - rmMock - .Setup(m => m.QuerySet) - .Returns(querySet); - - - var queryComposer = new QueryComposer(); - // act - var filterString = queryComposer.Compose(rmMock.Object); - // assert - Assert.Equal("&filter[attribute]=le:value&filter[attribute2]=value2", filterString); - } - - [Fact] - public void NoFilter_Compose_EmptyStringReturned() - { - // arrange - var querySet = new QuerySet(); - - var rmMock = new Mock(); - rmMock - .Setup(m => m.QuerySet) - .Returns(querySet); - - var queryComposer = new QueryComposer(); - // Act - - var filterString = queryComposer.Compose(rmMock.Object); - // assert - Assert.Equal("", filterString); - } - } -} diff --git a/test/UnitTests/Services/QueryParserTests.cs b/test/UnitTests/Services/QueryParserTests.cs deleted file mode 100644 index 04daa3cab4..0000000000 --- a/test/UnitTests/Services/QueryParserTests.cs +++ /dev/null @@ -1,398 +0,0 @@ -//using System.Collections.Generic; -//using System.Linq; -//using JsonApiDotNetCore.Configuration; -//using JsonApiDotNetCore.Controllers; -//using JsonApiDotNetCore.Internal; -//using JsonApiDotNetCore.Internal.Contracts; -//using JsonApiDotNetCore.Managers.Contracts; -//using JsonApiDotNetCore.Models; -//using JsonApiDotNetCore.Query; -//using JsonApiDotNetCore.Services; -//using Microsoft.AspNetCore.Http; -//using Microsoft.Extensions.Primitives; -//using Moq; -//using Xunit; - -//namespace UnitTests.Services -//{ -// public class QueryParserTests -// { -// private readonly Mock _requestMock; -// private readonly Mock _queryCollectionMock; -// private readonly Mock _pageQueryMock; -// private readonly ISparseFieldsService _sparseFieldsService = new Mock().Object; -// private readonly IIncludeService _includeService = new Mock().Object; -// private readonly IContextEntityProvider _graph = new Mock().Object; - -// public QueryParserTests() -// { -// _requestMock = new Mock(); -// _queryCollectionMock = new Mock(); -// _pageQueryMock = new Mock(); -// } - -// private QueryParser GetQueryParser() -// { -// return new QueryParser(new IncludeService(), _sparseFieldsService , _requestMock.Object, _graph, _pageQueryMock.Object, new JsonApiOptions()); -// } - -// [Fact] -// public void Can_Build_Filters() -// { -// // arrange -// var query = new Dictionary { -// { "filter[key]", new StringValues("value") } -// }; - -// _queryCollectionMock -// .Setup(m => m.GetEnumerator()) -// .Returns(query.GetEnumerator()); - -// _requestMock -// .Setup(m => m.DisabledQueryParams) -// .Returns(QueryParams.None); - -// var queryParser = GetQueryParser(); - -// // act -// var querySet = queryParser.Parse(_queryCollectionMock.Object); - -// // assert -// Assert.Equal("value", querySet.Filters.Single(f => f.Attribute == "key").Value); -// } - -// [Fact] -// public void Filters_Properly_Parses_DateTime_With_Operation() -// { -// // arrange -// const string dt = "2017-08-15T22:43:47.0156350-05:00"; -// var query = new Dictionary { -// { "filter[key]", new StringValues("le:" + dt) } -// }; - -// _queryCollectionMock -// .Setup(m => m.GetEnumerator()) -// .Returns(query.GetEnumerator()); - -// _requestMock -// .Setup(m => m.DisabledQueryParams) -// .Returns(QueryParams.None); - -// var queryParser = GetQueryParser(); - -// // act -// var querySet = queryParser.Parse(_queryCollectionMock.Object); - -// // assert -// Assert.Equal(dt, querySet.Filters.Single(f => f.Attribute == "key").Value); -// Assert.Equal("le", querySet.Filters.Single(f => f.Attribute == "key").Operation); -// } - -// [Fact] -// public void Filters_Properly_Parses_DateTime_Without_Operation() -// { -// // arrange -// const string dt = "2017-08-15T22:43:47.0156350-05:00"; -// var query = new Dictionary { -// { "filter[key]", new StringValues(dt) } -// }; - -// _queryCollectionMock -// .Setup(m => m.GetEnumerator()) -// .Returns(query.GetEnumerator()); - -// _requestMock -// .Setup(m => m.DisabledQueryParams) -// .Returns(QueryParams.None); - -// var queryParser = GetQueryParser(); - -// // act -// var querySet = queryParser.Parse(_queryCollectionMock.Object); - -// // assert -// Assert.Equal(dt, querySet.Filters.Single(f => f.Attribute == "key").Value); -// Assert.Equal(string.Empty, querySet.Filters.Single(f => f.Attribute == "key").Operation); -// } - -// [Fact] -// public void Can_Disable_Filters() -// { -// // Arrange -// var query = new Dictionary { -// { "filter[key]", new StringValues("value") } -// }; - -// _queryCollectionMock -// .Setup(m => m.GetEnumerator()) -// .Returns(query.GetEnumerator()); - -// _requestMock -// .Setup(m => m.DisabledQueryParams) -// .Returns(QueryParams.Filters); - -// var queryParser = GetQueryParser(); - -// // Act -// var querySet = queryParser.Parse(_queryCollectionMock.Object); - -// // Assert -// Assert.Empty(querySet.Filters); -// } -// [Theory] -// [InlineData("text,,1")] -// [InlineData("text,hello,,5")] -// [InlineData(",,2")] -// public void Parse_EmptySortSegment_ReceivesJsonApiException(string stringSortQuery) -// { -// // Arrange -// var query = new Dictionary { -// { "sort", new StringValues(stringSortQuery) } -// }; - -// _queryCollectionMock -// .Setup(m => m.GetEnumerator()) -// .Returns(query.GetEnumerator()); - -// var queryParser = GetQueryParser(); - -// // Act / Assert -// var exception = Assert.Throws(() => -// { -// var querySet = queryParser.Parse(_queryCollectionMock.Object); -// }); -// Assert.Contains("sort", exception.Message); -// } -// [Fact] -// public void Can_Disable_Sort() -// { -// // Arrange -// var query = new Dictionary { -// { "sort", new StringValues("-key") } -// }; - -// _queryCollectionMock -// .Setup(m => m.GetEnumerator()) -// .Returns(query.GetEnumerator()); - -// _requestMock -// .Setup(m => m.DisabledQueryParams) -// .Returns(QueryParams.Sort); - -// var queryParser = GetQueryParser(); - -// // act -// var querySet = queryParser.Parse(_queryCollectionMock.Object); - -// // assert -// Assert.Empty(querySet.SortParameters); -// } - -// [Fact] -// public void Can_Disable_Include() -// { -// // arrange -// var query = new Dictionary { -// { "include", new StringValues("key") } -// }; - -// _queryCollectionMock -// .Setup(m => m.GetEnumerator()) -// .Returns(query.GetEnumerator()); - -// _requestMock -// .Setup(m => m.DisabledQueryParams) -// .Returns(QueryParams.Include); - -// var queryParser = GetQueryParser(); - -// // act -// var querySet = queryParser.Parse(_queryCollectionMock.Object); - -// // assert -// Assert.Empty(querySet.IncludedRelationships); -// } - -// [Fact] -// public void Can_Disable_Page() -// { -// // arrange -// var query = new Dictionary { -// { "page[size]", new StringValues("1") } -// }; - -// _queryCollectionMock -// .Setup(m => m.GetEnumerator()) -// .Returns(query.GetEnumerator()); - -// _requestMock -// .Setup(m => m.DisabledQueryParams) -// .Returns(QueryParams.Page); - -// var queryParser = GetQueryParser(); - -// // act -// var querySet = queryParser.Parse(_queryCollectionMock.Object); - -// // assert -// Assert.Equal(null, querySet.PageQuery.PageSize); -// } - -// [Fact] -// public void Can_Disable_Fields() -// { -// // arrange -// var query = new Dictionary { -// { "fields", new StringValues("key") } -// }; - -// _queryCollectionMock -// .Setup(m => m.GetEnumerator()) -// .Returns(query.GetEnumerator()); - -// _requestMock -// .Setup(m => m.DisabledQueryParams) -// .Returns(QueryParams.Fields); - -// var queryParser = GetQueryParser(); - -// // act -// var querySet = queryParser.Parse(_queryCollectionMock.Object); - -// // Assert -// Assert.Empty(querySet.Fields); -// } - -// [Fact] -// public void Can_Parse_Fields_Query() -// { -// // arrange -// const string type = "articles"; -// const string attrName = "some-field"; -// const string internalAttrName = "SomeField"; - -// var query = new Dictionary { { $"fields[{type}]", new StringValues(attrName) } }; - -// _queryCollectionMock -// .Setup(m => m.GetEnumerator()) -// .Returns(query.GetEnumerator()); - -// _requestMock -// .Setup(m => m.GetRequestResource()) -// .Returns(new ContextEntity -// { -// EntityName = type, -// Attributes = new List -// { -// new AttrAttribute(attrName) -// { -// InternalAttributeName = internalAttrName -// } -// }, -// Relationships = new List() -// }); - -// var queryParser = GetQueryParser(); - -// // act -// var querySet = queryParser.Parse(_queryCollectionMock.Object); - -// // assert -// Assert.NotEmpty(querySet.Fields); -// Assert.Equal(2, querySet.Fields.Count); -// Assert.Equal("Id", querySet.Fields[0]); -// Assert.Equal(internalAttrName, querySet.Fields[1]); -// } - -// [Fact] -// public void Throws_JsonApiException_If_Field_DoesNotExist() -// { -// // arrange -// const string type = "articles"; -// const string attrName = "dne"; - -// var query = new Dictionary { { $"fields[{type}]", new StringValues(attrName) } }; - -// _queryCollectionMock -// .Setup(m => m.GetEnumerator()) -// .Returns(query.GetEnumerator()); - -// _requestMock -// .Setup(m => m.GetRequestResource()) -// .Returns(new ContextEntity -// { -// EntityName = type, -// Attributes = new List(), -// Relationships = new List() -// }); - -// var queryParser = GetQueryParser(); - -// // act , assert -// var ex = Assert.Throws(() => queryParser.Parse(_queryCollectionMock.Object)); -// Assert.Equal(400, ex.GetStatusCode()); -// } - - - -// [Theory] -// [InlineData("1", 1, false)] -// [InlineData("abcde", 0, true)] -// [InlineData("", 0, true)] -// public void Can_Parse_Page_Size_Query(string value, int expectedValue, bool shouldThrow) -// { -// // arrange -// var query = new Dictionary -// { { "page[size]", new StringValues(value) } -// }; - -// _queryCollectionMock -// .Setup(m => m.GetEnumerator()) -// .Returns(query.GetEnumerator()); - -// var queryParser = GetQueryParser(); - -// // act -// if (shouldThrow) -// { -// var ex = Assert.Throws(() => queryParser.Parse(_queryCollectionMock.Object)); -// Assert.Equal(400, ex.GetStatusCode()); -// } -// else -// { -// var querySet = queryParser.Parse(_queryCollectionMock.Object); -// Assert.Equal(expectedValue, querySet.PageQuery.PageSize); -// } -// } - -// [Theory] -// [InlineData("1", 1, false)] -// [InlineData("abcde", 0, true)] -// [InlineData("", 0, true)] -// public void Can_Parse_Page_Number_Query(string value, int expectedValue, bool shouldThrow) -// { -// // arrange -// var query = new Dictionary -// { { "page[number]", new StringValues(value) } -// }; - -// _queryCollectionMock -// .Setup(m => m.GetEnumerator()) -// .Returns(query.GetEnumerator()); - -// var queryParser = GetQueryParser(); - -// // act -// if (shouldThrow) -// { -// var ex = Assert.Throws(() => queryParser.Parse(_queryCollectionMock.Object)); -// Assert.Equal(400, ex.GetStatusCode()); -// } -// else -// { -// var querySet = queryParser.Parse(_queryCollectionMock.Object); -// Assert.Equal(expectedValue, querySet.PageQuery.PageOffset); -// } -// } -// } -//} diff --git a/test/UnitTests/TestModels.cs b/test/UnitTests/TestModels.cs index 9d5639d16f..8abcc84b8e 100644 --- a/test/UnitTests/TestModels.cs +++ b/test/UnitTests/TestModels.cs @@ -66,7 +66,7 @@ public class IdentifiableWithAttribute : Identifiable [Attr] public string AttributeMember { get; set; } } - internal class MultipleRelationshipsPrincipalPart : IdentifiableWithAttribute + public class MultipleRelationshipsPrincipalPart : IdentifiableWithAttribute { [HasOne] public OneToOneDependent PopulatedToOne { get; set; } [HasOne] public OneToOneDependent EmptyToOne { get; set; } @@ -92,6 +92,8 @@ public class Article : Identifiable [Attr] public string Title { get; set; } [HasOne] public Person Reviewer { get; set; } [HasOne] public Person Author { get; set; } + + [HasOne(canInclude: false)] public Person CannotInclude { get; set; } } public class Person : Identifiable diff --git a/wiki/v4/content/query-parameter-services.md b/wiki/v4/content/query-parameter-services.md new file mode 100644 index 0000000000..28aba58f51 --- /dev/null +++ b/wiki/v4/content/query-parameter-services.md @@ -0,0 +1,123 @@ +# Query Parameter Services + +This article describes +1. how URL query parameters are currently processed internally +2. how to customize the behaviour of existing query parameters +3. how to register your own + +## 1. Internal usage + +Below is a list of the query parameters that are supported. Each supported query parameter has it's own dedicated service. + +| Query Parameter Service | Occurence in URL | Domain | +|-------------------------|--------------------------------|-------------------------------------------------------| +| `IFilterService` | `?filter[article.title]=title` | filtering the resultset | +| `IIncludeService` | `?include=article.author` | including related data | +| `IPageService` | `?page[size]=10&page[number]=3` | pagination of the resultset | +| `ISortService` | `?sort=-title` | sorting the resultset | +| `ISparseFieldsService` | `?fields[article]=title,summary` | sparse field selection | +| `IOmitDefaultService` | `?omitDefault=true` | omitting default values from the serialization result, eg `guid-value": "00000000-0000-0000-0000-000000000000"` | +| `IOmitNullService` | `?omitNull=false` | omitting null values from the serialization result | + + +These services are responsible for parsing the value from the URL by gathering relevant (meta)data and performing validations as required by JsonApiDotNetCore down the pipeline. For example, the `IIncludeService` is responsible for checking if `article.author` is a valid relationship chain, and pre-processes the chain into a `List` so that the rest of the framework can process it easier. + +Each of these services implement the `IQueryParameterService` interface, which exposes: +* a `Name` property that is used internally to match the URL query parameter to the service. + `IIncludeService.Name` returns `include`, which will match `include=article.author` +* a `Parse` method that is called internally in the middleware to process the url query parameters. + + +```c# +public interface IQueryParameterService +{ + /// + /// Parses the value of the query parameter. Invoked in the middleware. + /// + /// the value of the query parameter as retrieved from the url + void Parse(KeyValuePair queryParameter); + /// + /// The name of the query parameter as matched in the URL query string. + /// + string Name { get; } +} +``` + +The piece of internals that is responsible for calling the `Parse` method is the `IQueryParameterDiscovery` service (formally known as `QueryParser`). This service injects every registered implementation of `IQueryParameterService` and calls the parse method with the appropiate part of the url querystring. + + +## 2. Customizing behaviour +You can register your own implementation of every service interface in the table above. As an example, we may want to add additional support for `page[index]=3` next to `page[number]=3` ("number" replaced with "index"). This could be achieved as follows + +```c# +// CustomPageService.cs +public class CustomPageService : PageService +{ + public override void Parse(KeyValuePair queryParameter) + { + var key = queryParameter.Key.Replace("index", "number"); + queryParameter = KeyValuePair(key, queryParameter.Value); + base.Parse(queryParameter) + } +} + +// Startup.cs +services.AddScoped(); +``` + +## 3. Registering new services +You may also define an entirely new custom query parameter. For example, we want to trigger a `HTTP 418 I'm a teapot` if a client includes a `?teapot=true` query parameter. This could be implemented as follows: + + +```c# +// ITeapotService.cs +public interface ITeapotService +{ + // Interface containing the "business logic" of the query parameter service, + // in a way useful to your application + bool ShouldThrowTeapot { get; } +} + +// TeapotService.cs +public class TeapotService : IQueryParameterService, ITeapotService +{ // ^^^ must inherit the IQueryParameterService interface + pubic bool ShouldThrowTeapot { get; } + + public string Name => "teapot"; + + public override void Parse(KeyValuePair queryParameter) + { + if(bool.Parse(queryParameter.Value, out bool config)) + ShouldThrowTeapot = true; + } +} + +// Startup.cs +services.AddScoped(); // exposes the parsed query parameter to your application +services.AddScoped(); // ensures that the associated query parameter service will be parsed internally by JADNC. +``` + +Things to pay attention to: +* The teapot service must be registered as an implementation of `IQueryParameterService` to be processed internally in the middleware +* Any other (business) logic is exposed on ITeapotService for usage in your application. + + +Now, we could access the custom query parameter service anywhere in our application to trigger a 418. Let's use the resource hooks to include this piece of business logic +```c# +public class TodoResource : ResourceDefinition +{ + private readonly ITeapotService _teapotService; + + public TodoResource(IResourceGraph graph, ITeapotService teapotService) : base(graph) + { + _teapotService = teapotService + } + + public override void BeforeRead(ResourcePipeline pipeline, bool isIncluded = false, string stringId = null) + { + if (teapotService.ShouldThrowTeapot) + throw new JsonApiException(418, "This is caused by the usage of teapot=true.") + } + +} +``` \ No newline at end of file diff --git a/wiki/v4/decoupling-architecture.md b/wiki/v4/decoupling-architecture.md index 30f3b0577b..8091fc8723 100644 --- a/wiki/v4/decoupling-architecture.md +++ b/wiki/v4/decoupling-architecture.md @@ -3,6 +3,7 @@ We upgraded to .NET Core 3.0. Furthermore, for V4 we have some explaining to do, namely the most notable changes: - [Serialization](./content/serialization.md) +- [Query Parameter Services](./content/query-parameter-services.md) - [Extendability](./content/extendability.md) -- [Testing](./content/testing.md) sdf +- [Testing](./content/testing.md) - [Deprecation](./content/deprecation.md)