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; }
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)); }
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)); }
/// <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)); }
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 }
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)); }
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 }); }
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))); }