internal static PreprocessResult PreprocessRequest(
            object content,
            HttpRequestMessage request,
            JsonApiConfiguration config)
        {
            var jsonApi = new JsonApiSerializer();
            jsonApi.JsonConverters.AddRange(config.JsonConverters);

            PrepareQueryContext(jsonApi, request, config);

            ApiResource resource = null;
            if (request.Properties.ContainsKey(Constants.RequestPropertyName))
            {
                resource = (ApiResource)request.Properties[Constants.RequestPropertyName];
            }
            else if (content != null && !(content is HttpError))
            {
                content = new JsonApiException(
                    ErrorType.Server,
                    "You must add a [ReturnsResourceAttribute] to action methods.")
                {
                    HelpLink = "https://github.com/joukevandermaas/saule/wiki"
                };
            }

            PrepareUrlPathBuilder(jsonApi, request, config);

            return jsonApi.PreprocessContent(content, resource, request.RequestUri);
        }
        public ApiError(Exception ex)
        {
            Title = ex.Message;
            Detail = ex.ToString();
            Code = ex.GetType().FullName;
            Links = ex.HelpLink != null
                ? new Dictionary<string, string> { ["about"] = ex.HelpLink }
                : null;

            _exception = ex as JsonApiException;
        }
Exemple #3
0
        public void Can_GetStatusCode()
        {
            var errors    = new ErrorCollection();
            var exception = new JsonApiException(errors);

            // Add First 422 error
            errors.Add(new Error(422, "Something wrong"));
            Assert.Equal(422, exception.GetStatusCode());

            // Add a second 422 error
            errors.Add(new Error(422, "Something else wrong"));
            Assert.Equal(422, exception.GetStatusCode());

            // Add 4xx error not 422
            errors.Add(new Error(401, "Unauthorized"));
            Assert.Equal(400, exception.GetStatusCode());

            // Add 5xx error not 4xx
            errors.Add(new Error(502, "Not good"));
            Assert.Equal(500, exception.GetStatusCode());
        }
        protected override async Task <IQueryable <TRelated> > GetRelatedQuery(string primaryResourceId,
                                                                               CancellationToken cancellationToken)
        {
            var param        = Expression.Parameter(typeof(TPrimaryResource));
            var accessorExpr = Expression.Property(param, _relationship.Property);
            var lambda       = Expression.Lambda <Func <TPrimaryResource, IEnumerable <TRelated> > >(accessorExpr, param);

            var primaryEntityQuery = FilterById <TPrimaryResource>(primaryResourceId, _primaryTypeRegistration);

            // We have to see if the resource even exists, so we can throw a 404 if it doesn't
            var relatedResource = await primaryEntityQuery.FirstOrDefaultAsync(cancellationToken);

            if (relatedResource == null)
            {
                throw JsonApiException.CreateForNotFound(string.Format(
                                                             "No resource of type `{0}` exists with id `{1}`.",
                                                             _primaryTypeRegistration.ResourceTypeName, primaryResourceId));
            }

            return(primaryEntityQuery.SelectMany(lambda));
        }
Exemple #5
0
        private Expression GetPredicate(string filterField, IResourceTypeRegistration registration, ParameterExpression param, string queryValue)
        {
            if (filterField == "id")
            {
                return(GetPredicateBodyForProperty(registration.IdProperty, queryValue, param));
            }

            var resourceTypeField = registration.GetFieldByName(filterField);

            if (resourceTypeField == null)
            {
                throw JsonApiException.CreateForBadRequest(
                          string.Format("No attribute {0} exists on the specified type.", filterField));
            }

            if (string.IsNullOrWhiteSpace(queryValue))
            {
                queryValue = null;
            }

            // See if it is a field property
            var fieldModelProperty = resourceTypeField as ResourceTypeAttribute;

            if (fieldModelProperty != null)
            {
                return(GetPredicateBodyForField(fieldModelProperty, queryValue, param));
            }

            // See if it is a relationship property
            var relationshipModelProperty = resourceTypeField as ResourceTypeRelationship;

            if (relationshipModelProperty != null)
            {
                return(GetPredicateBodyForRelationship(relationshipModelProperty, queryValue, param));
            }

            throw JsonApiException.CreateForBadRequest(
                      string.Format("The attribute {0} is unsupported for filtering.", filterField));
        }
Exemple #6
0
        /// <summary>
        /// Generic method for getting the related resources for a to-many relationship
        /// </summary>
        protected async Task <IResourceCollectionDocument> GetRelatedToMany <TRelated>(string id,
                                                                                       ResourceTypeRelationship relationship, HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var param        = Expression.Parameter(typeof(T));
            var accessorExpr = Expression.Property(param, relationship.Property);
            var lambda       = Expression.Lambda <Func <T, IEnumerable <TRelated> > >(accessorExpr, param);

            var primaryEntityQuery = FilterById <T>(id, _resourceTypeRegistration);

            // We have to see if the resource even exists, so we can throw a 404 if it doesn't
            var relatedResource = await primaryEntityQuery.FirstOrDefaultAsync(cancellationToken);

            if (relatedResource == null)
            {
                throw JsonApiException.CreateForNotFound(string.Format("No resource of type `{0}` exists with id `{1}`.",
                                                                       _resourceTypeRegistration.ResourceTypeName, id));
            }

            var relatedResourceQuery = primaryEntityQuery.SelectMany(lambda);
            var sortExpressions      = _sortExpressionExtractor.ExtractSortExpressions(request);

            return(await _queryableResourceCollectionDocumentBuilder.BuildDocument(relatedResourceQuery, request, sortExpressions, cancellationToken));
        }
Exemple #7
0
        private Expression GetPredicateBody(HttpRequestMessage request, ParameterExpression param)
        {
            Expression workingExpr = null;

            var type       = param.Type;
            var queryPairs = request.GetQueryNameValuePairs();

            foreach (var queryPair in queryPairs)
            {
                if (String.IsNullOrWhiteSpace(queryPair.Key))
                {
                    continue;
                }

                if (!queryPair.Key.StartsWith("filter."))
                {
                    continue;
                }

                var filterField = queryPair.Key.Substring(7); // Skip "filter."

                IResourceTypeRegistration registration;
                try
                {
                    registration = _resourceTypeRegistry.GetRegistrationForType(type);
                }
                catch (TypeRegistrationNotFoundException)
                {
                    throw JsonApiException.CreateForBadRequest("No registration exists for the specified type");
                }

                var expr = GetPredicate(filterField, registration, param, queryPair.Value);
                workingExpr = workingExpr == null ? expr : Expression.AndAlso(workingExpr, expr);
            }

            return(workingExpr ?? Expression.Constant(true)); // No filters, so return everything
        }
Exemple #8
0
        internal static PreprocessResult PreprocessRequest(
            object content,
            HttpRequestMessage request,
            JsonApiConfiguration config)
        {
            var jsonApi = new JsonApiSerializer();

            jsonApi.JsonConverters.AddRange(config.JsonConverters);

            PrepareQueryContext(jsonApi, request, config);

            ApiResource resource    = null;
            bool        isHttpError = content is HttpError || content is IEnumerable <HttpError>;

            if (request.Properties.ContainsKey(Constants.PropertyNames.ResourceDescriptor))
            {
                resource = (ApiResource)request.Properties[Constants.PropertyNames.ResourceDescriptor];
            }
            else if (content != null && !isHttpError)
            {
                content = new JsonApiException(
                    ErrorType.Server,
                    "You must add a [ReturnsResourceAttribute] to action methods.")
                {
                    HelpLink = "https://github.com/joukevandermaas/saule/wiki"
                };
            }

            if (!isHttpError && jsonApi.QueryContext?.Pagination?.PerPage > jsonApi.QueryContext?.Pagination?.PageSizeLimit)
            {
                content = new JsonApiException(ErrorType.Client, "Page size exceeds page size limit for queries.");
            }

            PrepareUrlPathBuilder(jsonApi, request, config);

            return(jsonApi.PreprocessContent(content, resource, request.RequestUri, config));
        }
Exemple #9
0
        protected override void Load(ContainerBuilder builder)
        {
            // Register resource types
            var registry = new ResourceTypeRegistry();

            foreach (var resourceTypeConfiguration in _jsonApiConfiguration.ResourceTypeConfigurations)
            {
                var resourceTypeRegistration = resourceTypeConfiguration.BuildResourceTypeRegistration();
                registry.AddRegistration(resourceTypeRegistration);

                var configuration = resourceTypeConfiguration;
                builder.Register(c => configuration)
                .Keyed <IResourceTypeConfiguration>(resourceTypeRegistration.Type)
                .Keyed <IResourceTypeConfiguration>(resourceTypeRegistration.ResourceTypeName)
                .SingleInstance();

                if (resourceTypeConfiguration.DocumentMaterializerType != null)
                {
                    builder.RegisterType(resourceTypeConfiguration.DocumentMaterializerType);
                }
                if (resourceTypeConfiguration.ResourceCollectionResolverType != null)
                {
                    builder.RegisterType(resourceTypeConfiguration.ResourceCollectionResolverType);
                }

                foreach (var relationship in resourceTypeRegistration.Relationships)
                {
                    IResourceTypeRelationshipConfiguration relationshipConfiguration;
                    if (resourceTypeConfiguration.RelationshipConfigurations
                        .TryGetValue(relationship.Property.Name, out relationshipConfiguration))
                    {
                        if (relationshipConfiguration.MaterializerType != null)
                        {
                            builder.RegisterType(relationshipConfiguration.MaterializerType);
                            continue;
                        }
                    }

                    // They didn't set an explicit materializer. See if they specified a factory for this resource type.
                    if (configuration.RelatedResourceMaterializerTypeFactory == null)
                    {
                        continue;
                    }

                    var materializerType = configuration.RelatedResourceMaterializerTypeFactory(relationship);
                    builder.RegisterType(materializerType);
                }
            }

            builder.Register(c => registry).As <IResourceTypeRegistry>().SingleInstance();
            builder.Register(c =>
            {
                var context = c.Resolve <IComponentContext>();
                Func <string, IDocumentMaterializer> factory = resourceTypeName =>
                {
                    var configuration = context.ResolveKeyed <IResourceTypeConfiguration>(resourceTypeName);
                    var registration  = registry.GetRegistrationForResourceTypeName(resourceTypeName);
                    var parameters    = new Parameter[] { new TypedParameter(typeof(IResourceTypeRegistration), registration) };
                    if (configuration.ResourceCollectionResolverType != null)
                    {
                        var collectionResolver = context.Resolve(configuration.ResourceCollectionResolverType, parameters);
                        parameters             = new Parameter[] { new TypedParameter(typeof(IResourceTypeRegistration), registration), new NamedParameter("collectionResolver", collectionResolver), };
                    }
                    if (configuration.DocumentMaterializerType != null)
                    {
                        return((IDocumentMaterializer)context.Resolve(configuration.DocumentMaterializerType, parameters));
                    }
                    return(context.Resolve <IDocumentMaterializer>(parameters));
                };
                return(factory);
            });
            builder.Register(c =>
            {
                var context = c.Resolve <IComponentContext>();
                Func <Type, IDocumentMaterializer> factory = clrType =>
                {
                    var configuration = context.ResolveKeyed <IResourceTypeConfiguration>(clrType);
                    var registration  = registry.GetRegistrationForType(clrType);
                    var parameters    = new List <Parameter> {
                        new TypedParameter(typeof(IResourceTypeRegistration), registration)
                    };

                    // add parameter for collectionResolver
                    if (configuration.ResourceCollectionResolverType != null)
                    {
                        var collectionResolver = context.Resolve(configuration.ResourceCollectionResolverType, parameters);
                        parameters.Add(new NamedParameter("collectionResolver", collectionResolver));
                    }

                    if (configuration.DocumentMaterializerType != null)
                    {
                        return((IDocumentMaterializer)context.Resolve(configuration.DocumentMaterializerType, parameters));
                    }
                    return(context.Resolve <IDocumentMaterializer>(parameters));
                };
                return(factory);
            });
            builder.Register(c =>
            {
                var context = c.Resolve <IComponentContext>();
                Func <string, string, IRelatedResourceDocumentMaterializer> factory = (resourceTypeName, relationshipName) =>
                {
                    var configuration = context.ResolveKeyed <IResourceTypeConfiguration>(resourceTypeName);
                    var registration  = registry.GetRegistrationForResourceTypeName(resourceTypeName);
                    var relationship  = registration.GetFieldByName(relationshipName) as ResourceTypeRelationship;
                    if (relationship == null)
                    {
                        throw JsonApiException.CreateForNotFound(
                            string.Format("No relationship `{0}` exists for the resource type `{1}`.", relationshipName, resourceTypeName));
                    }

                    var parameters = new List <Parameter>
                    {
                        new TypedParameter(typeof(IResourceTypeRegistration), registration),
                        new TypedParameter(typeof(ResourceTypeRelationship), relationship)
                    };

                    // add parameter for collectionResolver
                    if (context.IsRegisteredWithKey <IResourceTypeConfiguration>(relationship.RelatedType))
                    {
                        var relConfiguration = context.ResolveKeyed <IResourceTypeConfiguration>(relationship.RelatedType);
                        if (relConfiguration.ResourceCollectionResolverType != null)
                        {
                            var collectionResolver = context.Resolve(relConfiguration.ResourceCollectionResolverType, parameters);
                            parameters.Add(new NamedParameter("collectionResolver", collectionResolver));
                        }
                    }

                    // First, see if they have set an explicit materializer for this relationship
                    IResourceTypeRelationshipConfiguration relationshipConfiguration;
                    if (configuration.RelationshipConfigurations.TryGetValue(relationship.Property.Name,
                                                                             out relationshipConfiguration) && relationshipConfiguration.MaterializerType != null)
                    {
                        return((IRelatedResourceDocumentMaterializer)context.Resolve(relationshipConfiguration.MaterializerType, parameters));
                    }

                    // They didn't set an explicit materializer. See if they specified a factory for this resource type.
                    if (configuration.RelatedResourceMaterializerTypeFactory != null)
                    {
                        var materializerType = configuration.RelatedResourceMaterializerTypeFactory(relationship);
                        return((IRelatedResourceDocumentMaterializer)context.Resolve(materializerType, parameters));
                    }

                    return(context.Resolve <IRelatedResourceDocumentMaterializer>(parameters));
                };
                return(factory);
            });

            builder.RegisterType <JsonApiHttpConfiguration>().SingleInstance();
            if (_jsonApiConfiguration.CustomBaseUrlService != null)
            {
                builder.Register(c => _jsonApiConfiguration.CustomBaseUrlService).As <IBaseUrlService>().SingleInstance();
            }
            else
            {
                builder.RegisterType <BaseUrlService>().As <IBaseUrlService>().SingleInstance();
            }
            builder.RegisterType <DocumentMaterializerLocator>().As <IDocumentMaterializerLocator>().InstancePerRequest();

            // Serialization
            builder.RegisterType <MetadataFormatter>().As <IMetadataFormatter>().SingleInstance();
            builder.RegisterType <LinkFormatter>().As <ILinkFormatter>().SingleInstance();
            builder.RegisterType <ResourceLinkageFormatter>().As <IResourceLinkageFormatter>().SingleInstance();
            builder.RegisterType <RelationshipObjectFormatter>().As <IRelationshipObjectFormatter>().SingleInstance();
            builder.RegisterType <ResourceObjectFormatter>().As <IResourceObjectFormatter>().SingleInstance();
            builder.RegisterType <SingleResourceDocumentFormatter>().As <ISingleResourceDocumentFormatter>().SingleInstance();
            builder.RegisterType <ResourceCollectionDocumentFormatter>().As <IResourceCollectionDocumentFormatter>().SingleInstance();
            builder.RegisterType <ErrorFormatter>().As <IErrorFormatter>().SingleInstance();
            builder.RegisterType <ErrorDocumentFormatter>().As <IErrorDocumentFormatter>().SingleInstance();

            // Queryable transforms
            builder.RegisterType <SynchronousEnumerationTransformer>().As <IQueryableEnumerationTransformer>().SingleInstance();
            builder.RegisterType <DefaultFilteringTransformer>().As <IQueryableFilteringTransformer>().SingleInstance();
            builder.RegisterType <DefaultSortingTransformer>().As <IQueryableSortingTransformer>().SingleInstance();
            builder.RegisterType <DefaultPaginationTransformer>().As <IQueryablePaginationTransformer>().SingleInstance();

            // Document building
            builder.Register(c => _jsonApiConfiguration.LinkConventions).As <ILinkConventions>().SingleInstance();
            builder.RegisterType <JsonApiFormatter>().SingleInstance();
            builder.RegisterType <RegistryDrivenResourceCollectionDocumentBuilder>().As <IResourceCollectionDocumentBuilder>().SingleInstance();
            builder.RegisterType <RegistryDrivenSingleResourceDocumentBuilder>().As <ISingleResourceDocumentBuilder>().SingleInstance();
            builder.RegisterType <FallbackDocumentBuilder>().As <IFallbackDocumentBuilder>().SingleInstance();
            builder.RegisterType <ErrorDocumentBuilder>().As <IErrorDocumentBuilder>().SingleInstance();
            builder.RegisterType <FallbackDocumentBuilderAttribute>().SingleInstance();
            builder.RegisterType <JsonApiExceptionFilterAttribute>().SingleInstance();
            builder.RegisterType <DefaultQueryableResourceCollectionDocumentBuilder>().As <IQueryableResourceCollectionDocumentBuilder>();

            // Misc
            builder.RegisterType <DefaultSortExpressionExtractor>().As <ISortExpressionExtractor>().SingleInstance();
            builder.RegisterType <DefaultIncludeExpressionExtractor>().As <IIncludeExpressionExtractor>().SingleInstance();
        }
        public IPaginationTransformResult <T> ApplyPagination <T>(IQueryable <T> query, HttpRequestMessage request)
        {
            var hasPageNumberParam = false;
            var hasPageSizeParam   = false;
            var pageNumber         = 0;
            var pageSize           = _maxPageSize ?? DefaultPageSize;

            foreach (var kvp in request.GetQueryNameValuePairs())
            {
                if (kvp.Key == PageNumberQueryParam)
                {
                    hasPageNumberParam = true;
                    if (!int.TryParse(kvp.Value, out pageNumber))
                    {
                        throw JsonApiException.CreateForParameterError("Invalid page number",
                                                                       "Page number must be a positive integer.", PageNumberQueryParam);
                    }
                }
                else if (kvp.Key == PageSizeQueryParam)
                {
                    hasPageSizeParam = true;
                    if (!int.TryParse(kvp.Value, out pageSize))
                    {
                        throw JsonApiException.CreateForParameterError("Invalid page size",
                                                                       "Page size must be a positive integer.", PageSizeQueryParam);
                    }
                }
            }

            if (!hasPageNumberParam && !hasPageSizeParam)
            {
                return(new DefaultPaginationTransformResult <T>
                {
                    PagedQuery = query,
                    PaginationWasApplied = false
                });
            }

            if ((hasPageNumberParam && !hasPageSizeParam))
            {
                throw JsonApiException.CreateForParameterError("Page size missing",
                                                               string.Format("In order for paging to work properly, if either {0} or {1} is set, both must be.",
                                                                             PageNumberQueryParam, PageSizeQueryParam), PageNumberQueryParam);
            }

            if (pageNumber < 0)
            {
                throw JsonApiException.CreateForParameterError("Page number out of bounds",
                                                               "Page number must not be negative.", PageNumberQueryParam);
            }

            if (pageSize <= 0)
            {
                throw JsonApiException.CreateForParameterError("Page size out of bounds",
                                                               "Page size must be greater than or equal to 1.", PageSizeQueryParam);
            }

            if (_maxPageSize != null && pageSize > _maxPageSize.Value)
            {
                pageSize = _maxPageSize.Value;
            }

            if (pageNumber > 0)
            {
                pageNumber -= 1; // pagination is 1 based in frontend but zero based in backend!
            }

            var skip = pageNumber * pageSize;

            return(new DefaultPaginationTransformResult <T>
            {
                PageNumber = pageNumber,
                PageSize = pageSize,
                PagedQuery = query.Skip(skip).Take(pageSize),
                PaginationWasApplied = true
            });
        }
Exemple #11
0
        private Expression GetPredicateBodyForRelationship(ResourceTypeRelationship resourceTypeProperty, string queryValue, ParameterExpression param)
        {
            var          relatedType = resourceTypeProperty.RelatedType;
            PropertyInfo relatedIdProperty;

            try
            {
                var registration = _resourceTypeRegistry.GetRegistrationForType(relatedType);
                relatedIdProperty = registration.IdProperty;
            }
            catch (TypeRegistrationNotFoundException)
            {
                throw JsonApiException.CreateForBadRequest("No registration exists for the specified type");
            }

            var prop = resourceTypeProperty.Property;

            if (resourceTypeProperty.IsToMany)
            {
                var propertyExpr = Expression.Property(param, prop);

                if (string.IsNullOrWhiteSpace(queryValue))
                {
                    var leftExpr = Expression.Equal(propertyExpr, Expression.Constant(null));

                    var asQueryableCallExpr = Expression.Call(
                        typeof(Queryable),
                        "AsQueryable",
                        new[] { relatedType },
                        propertyExpr);
                    var anyCallExpr = Expression.Call(
                        typeof(Queryable),
                        "Any",
                        new[] { relatedType },
                        asQueryableCallExpr);
                    var rightExpr = Expression.Not(anyCallExpr);

                    return(Expression.OrElse(leftExpr, rightExpr));
                }
                else
                {
                    var leftExpr = Expression.NotEqual(propertyExpr, Expression.Constant(null));

                    var idValue  = queryValue.Trim();
                    var idExpr   = Expression.Constant(idValue);
                    var anyParam = Expression.Parameter(relatedType);
                    var relatedIdPropertyExpr         = Expression.Property(anyParam, relatedIdProperty);
                    var relatedIdPropertyEqualsIdExpr = Expression.Equal(relatedIdPropertyExpr, idExpr);
                    var anyPredicateExpr    = Expression.Lambda(relatedIdPropertyEqualsIdExpr, anyParam);
                    var asQueryableCallExpr = Expression.Call(
                        typeof(Queryable),
                        "AsQueryable",
                        new[] { relatedType },
                        propertyExpr);
                    var rightExpr = Expression.Call(
                        typeof(Queryable),
                        "Any",
                        new[] { relatedType },
                        asQueryableCallExpr,
                        anyPredicateExpr);

                    return(Expression.AndAlso(leftExpr, rightExpr));
                }
            }
            else
            {
                var propertyExpr = Expression.Property(param, prop);

                if (string.IsNullOrWhiteSpace(queryValue))
                {
                    return(Expression.Equal(propertyExpr, Expression.Constant(null)));
                }

                var leftExpr = Expression.NotEqual(propertyExpr, Expression.Constant(null));

                var idValue = queryValue.Trim();
                var idExpr  = Expression.Constant(idValue);
                var relatedIdPropertyExpr = Expression.Property(propertyExpr, relatedIdProperty);
                var rightExpr             = Expression.Equal(relatedIdPropertyExpr, idExpr);

                return(Expression.AndAlso(leftExpr, rightExpr));
            }
        }
        public IOrderedQueryable <T> Sort <T>(IQueryable <T> query, string[] sortExpressions)
        {
            if (sortExpressions == null || sortExpressions.Length == 0)
            {
                sortExpressions = new [] { "id" }
            }
            ;

            var selectors      = new List <ISelector <T> >();
            var usedProperties = new Dictionary <PropertyInfo, object>();

            var registration = _resourceTypeRegistry.GetRegistrationForType(typeof(T));

            foreach (var sortExpression in sortExpressions)
            {
                if (string.IsNullOrEmpty(sortExpression))
                {
                    throw JsonApiException.CreateForParameterError("Empty sort expression", "One of the sort expressions is empty.", "sort");
                }

                bool   ascending;
                string fieldName;
                if (sortExpression[0] == '-')
                {
                    ascending = false;
                    fieldName = sortExpression.Substring(1);
                }
                else
                {
                    ascending = true;
                    fieldName = sortExpression;
                }

                if (string.IsNullOrWhiteSpace(fieldName))
                {
                    throw JsonApiException.CreateForParameterError("Empty sort expression", "One of the sort expressions is empty.", "sort");
                }

                var        paramExpr = Expression.Parameter(typeof(T));
                Expression sortValueExpression;

                if (fieldName == "id")
                {
                    sortValueExpression = registration.GetSortByIdExpression(paramExpr);
                }
                else
                {
                    var modelProperty = registration.GetFieldByName(fieldName);
                    if (modelProperty == null)
                    {
                        throw JsonApiException.CreateForParameterError("Attribute not found",
                                                                       string.Format("The attribute \"{0}\" does not exist on type \"{1}\".",
                                                                                     fieldName, registration.ResourceTypeName), "sort");
                    }

                    var property = modelProperty.Property;

                    if (usedProperties.ContainsKey(property))
                    {
                        throw JsonApiException.CreateForParameterError("Attribute specified more than once",
                                                                       string.Format("The attribute \"{0}\" was specified more than once.", fieldName), "sort");
                    }

                    usedProperties[property] = null;
                    sortValueExpression      = Expression.Property(paramExpr, property);
                }

                var selector = GetSelector <T>(paramExpr, sortValueExpression, !ascending);
                selectors.Add(selector);
            }

            var firstSelector = selectors.First();

            IOrderedQueryable <T> workingQuery = firstSelector.ApplyInitially(query);

            return(selectors.Skip(1).Aggregate(workingQuery, (current, selector) => selector.ApplySubsequently(current)));
        }