diff --git a/src/JsonApiDotNetCore/Internal/ResourceGraph.cs b/src/JsonApiDotNetCore/Internal/ResourceGraph.cs index 7b25a93180..ee056ad4c2 100644 --- a/src/JsonApiDotNetCore/Internal/ResourceGraph.cs +++ b/src/JsonApiDotNetCore/Internal/ResourceGraph.cs @@ -84,7 +84,9 @@ private IEnumerable Getter(Expression> selec var targeted = new List(); - if (selector.Body is MemberExpression memberExpression) + var selectorBody = RemoveConvert(selector.Body); + + if (selectorBody is MemberExpression memberExpression) { // model => model.Field1 try { @@ -97,8 +99,7 @@ private IEnumerable Getter(Expression> selec } } - - if (selector.Body is NewExpression newExpression) + if (selectorBody is NewExpression newExpression) { // model => new { model.Field1, model.Field2 } string memberName = null; try @@ -119,11 +120,17 @@ private IEnumerable Getter(Expression> selec } } - throw new ArgumentException($"The expression returned by '{selector}' for '{GetType()}' is of type {selector.Body.GetType()}" - + " and cannot be used to select resource attributes. The type must be a NewExpression.Example: article => new { article.Author };"); - + throw new ArgumentException( + $"The expression '{selector}' should select a single property or select multiple properties into an anonymous type. " + + $"For example: 'article => article.Title' or 'article => new {{ article.Title, article.PageCount }}'."); } + private static Expression RemoveConvert(Expression expression) + => expression is UnaryExpression unaryExpression + && unaryExpression.NodeType == ExpressionType.Convert + ? RemoveConvert(unaryExpression.Operand) + : expression; + private void ThrowNotExposedError(string memberName, FieldFilterType type) { throw new ArgumentException($"{memberName} is not an json:api exposed {type:g}."); diff --git a/test/UnitTests/Models/ResourceDefinitionTests.cs b/test/UnitTests/Models/ResourceDefinitionTests.cs index 682d9e10d5..9beb4d6de9 100644 --- a/test/UnitTests/Models/ResourceDefinitionTests.cs +++ b/test/UnitTests/Models/ResourceDefinitionTests.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using JsonApiDotNetCore.Builders; using JsonApiDotNetCore.Internal.Query; @@ -23,7 +24,7 @@ public void Property_Sort_Order_Uses_NewExpression() // Assert Assert.Equal(2, sorts.Count); - Assert.Equal(nameof(Model.Prop), sorts[0].Attribute.PropertyInfo.Name); + Assert.Equal(nameof(Model.CreatedAt), sorts[0].Attribute.PropertyInfo.Name); Assert.Equal(SortDirection.Ascending, sorts[0].SortDirection); Assert.Equal(nameof(Model.Password), sorts[1].Attribute.PropertyInfo.Name); @@ -62,7 +63,7 @@ public class Model : Identifiable { [Attr] public string AlwaysExcluded { get; set; } [Attr] public string Password { get; set; } - [Attr] public string Prop { get; set; } + [Attr] public DateTime CreatedAt { get; set; } } public sealed class RequestFilteredResource : ResourceDefinition @@ -72,19 +73,21 @@ public sealed class RequestFilteredResource : ResourceDefinition public RequestFilteredResource(bool isAdmin) : base(new ResourceGraphBuilder(new JsonApiOptions(), NullLoggerFactory.Instance).AddResource().Build()) { if (isAdmin) - HideFields(m => m.AlwaysExcluded); + HideFields(model => model.AlwaysExcluded); else - HideFields(m => new { m.AlwaysExcluded, m.Password }); + HideFields(model => new { model.AlwaysExcluded, model.Password }); } public override QueryFilters GetQueryFilters() => new QueryFilters { { "is-active", (query, value) => query.Select(x => x) } }; + public override PropertySortOrder GetDefaultSortOrder() - => new PropertySortOrder { - (t => t.Prop, SortDirection.Ascending), - (t => t.Password, SortDirection.Descending) + => new PropertySortOrder + { + (model => model.CreatedAt, SortDirection.Ascending), + (model => model.Password, SortDirection.Descending) }; } }