Skip to content

Commit 4834612

Browse files
author
Bart Koelman
committed
Merge branch 'master' into various-fixes
2 parents 69e3e33 + c259150 commit 4834612

File tree

20 files changed

+159
-66
lines changed

20 files changed

+159
-66
lines changed

src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public string BirthCountryName
6565
[EagerLoad]
6666
public Country BirthCountry { get; set; }
6767

68-
[Attr(isImmutable: true)]
68+
[Attr(AttrCapabilities.All & ~AttrCapabilities.AllowMutate)]
6969
[NotMapped]
7070
public string GrantedVisaCountries => GrantedVisas == null || !GrantedVisas.Any()
7171
? null

src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ public string AlwaysChangingValue
3232
[Attr]
3333
public DateTime CreatedDate { get; set; }
3434

35-
[Attr(isFilterable: false, isSortable: false)]
35+
[Attr(AttrCapabilities.All & ~(AttrCapabilities.AllowFilter | AttrCapabilities.AllowSort))]
3636
public DateTime? AchievedDate { get; set; }
3737

3838
[Attr]
3939
public DateTime? UpdatedDate { get; set; }
4040

41-
[Attr(isImmutable: true)]
41+
[Attr(AttrCapabilities.All & ~AttrCapabilities.AllowMutate)]
4242
public string CalculatedValue => "calculated";
4343

4444
[Attr]

src/JsonApiDotNetCore/Builders/ResourceGraphBuilder.cs

+16-12
Original file line numberDiff line numberDiff line change
@@ -88,30 +88,35 @@ protected virtual List<AttrAttribute> GetAttributes(Type entityType)
8888
{
8989
var attributes = new List<AttrAttribute>();
9090

91-
var properties = entityType.GetProperties();
92-
93-
foreach (var prop in properties)
91+
foreach (var property in entityType.GetProperties())
9492
{
95-
// todo: investigate why this is added in the exposed attributes list
93+
var attribute = (AttrAttribute)property.GetCustomAttribute(typeof(AttrAttribute));
94+
95+
// TODO: investigate why this is added in the exposed attributes list
9696
// because it is not really defined attribute considered from the json:api
9797
// spec point of view.
98-
if (prop.Name == nameof(Identifiable.Id))
98+
if (property.Name == nameof(Identifiable.Id) && attribute == null)
9999
{
100100
var idAttr = new AttrAttribute
101101
{
102-
PublicAttributeName = FormatPropertyName(prop),
103-
PropertyInfo = prop
102+
PublicAttributeName = FormatPropertyName(property),
103+
PropertyInfo = property,
104+
Capabilities = _options.DefaultAttrCapabilities
104105
};
105106
attributes.Add(idAttr);
106107
continue;
107108
}
108109

109-
var attribute = (AttrAttribute)prop.GetCustomAttribute(typeof(AttrAttribute));
110110
if (attribute == null)
111111
continue;
112112

113-
attribute.PublicAttributeName ??= FormatPropertyName(prop);
114-
attribute.PropertyInfo = prop;
113+
attribute.PublicAttributeName ??= FormatPropertyName(property);
114+
attribute.PropertyInfo = property;
115+
116+
if (!attribute.HasExplicitCapabilities)
117+
{
118+
attribute.Capabilities = _options.DefaultAttrCapabilities;
119+
}
115120

116121
attributes.Add(attribute);
117122
}
@@ -238,8 +243,7 @@ private string FormatResourceName(Type resourceType)
238243

239244
private string FormatPropertyName(PropertyInfo resourceProperty)
240245
{
241-
var contractResolver = (DefaultContractResolver)_options.SerializerSettings.ContractResolver;
242-
return contractResolver.NamingStrategy.GetPropertyName(resourceProperty.Name, false);
246+
return _options.SerializerContractResolver.NamingStrategy.GetPropertyName(resourceProperty.Name, false);
243247
}
244248
}
245249
}

src/JsonApiDotNetCore/Configuration/IJsonApiOptions.cs

+10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
2+
using JsonApiDotNetCore.Models;
23
using JsonApiDotNetCore.Models.JsonApiDocuments;
34
using Newtonsoft.Json;
5+
using Newtonsoft.Json.Serialization;
46

57
namespace JsonApiDotNetCore.Configuration
68
{
@@ -59,5 +61,13 @@ public interface IJsonApiOptions : ILinksConfiguration
5961
/// </example>
6062
/// </summary>
6163
JsonSerializerSettings SerializerSettings { get; }
64+
65+
internal DefaultContractResolver SerializerContractResolver => (DefaultContractResolver)SerializerSettings.ContractResolver;
66+
67+
/// <summary>
68+
/// Specifies the default query string capabilities that can be used on exposed json:api attributes.
69+
/// Defaults to <see cref="AttrCapabilities.All"/>.
70+
/// </summary>
71+
AttrCapabilities DefaultAttrCapabilities { get; }
6272
}
6373
}

src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using JsonApiDotNetCore.Graph;
2+
using JsonApiDotNetCore.Models;
23
using JsonApiDotNetCore.Models.Links;
34
using Newtonsoft.Json;
45
using Newtonsoft.Json.Serialization;
@@ -58,6 +59,9 @@ public class JsonApiOptions : IJsonApiOptions
5859
/// <inheritdoc/>
5960
public bool AllowQueryStringOverrideForSerializerDefaultValueHandling { get; set; }
6061

62+
/// <inheritdoc/>
63+
public AttrCapabilities DefaultAttrCapabilities { get; } = AttrCapabilities.All;
64+
6165
/// <summary>
6266
/// The default page size for all resources. The value zero means: no paging.
6367
/// </summary>

src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs

+9-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using JsonApiDotNetCore.Services;
77
using Microsoft.AspNetCore.Mvc;
88
using Microsoft.Extensions.Logging;
9+
using Newtonsoft.Json.Serialization;
910

1011
namespace JsonApiDotNetCore.Controllers
1112
{
@@ -114,7 +115,10 @@ public virtual async Task<IActionResult> PostAsync([FromBody] T entity)
114115
throw new ResourceIdInPostRequestNotAllowedException();
115116

116117
if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid)
117-
throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions.IncludeExceptionStackTraceInErrors);
118+
{
119+
var namingStrategy = _jsonApiOptions.SerializerContractResolver.NamingStrategy;
120+
throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions.IncludeExceptionStackTraceInErrors, namingStrategy);
121+
}
118122

119123
entity = await _create.CreateAsync(entity);
120124

@@ -130,7 +134,10 @@ public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] T entity)
130134
throw new InvalidRequestBodyException(null, null, null);
131135

132136
if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid)
133-
throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions.IncludeExceptionStackTraceInErrors);
137+
{
138+
var namingStrategy = _jsonApiOptions.SerializerContractResolver.NamingStrategy;
139+
throw new InvalidModelStateException(ModelState, typeof(T), _jsonApiOptions.IncludeExceptionStackTraceInErrors, namingStrategy);
140+
}
134141

135142
var updatedEntity = await _update.UpdateAsync(id, entity);
136143
return updatedEntity == null ? Ok(null) : Ok(updatedEntity);

src/JsonApiDotNetCore/Exceptions/InvalidModelStateException.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using JsonApiDotNetCore.Models;
77
using JsonApiDotNetCore.Models.JsonApiDocuments;
88
using Microsoft.AspNetCore.Mvc.ModelBinding;
9+
using Newtonsoft.Json.Serialization;
910

1011
namespace JsonApiDotNetCore.Exceptions
1112
{
@@ -17,13 +18,13 @@ public class InvalidModelStateException : Exception
1718
public IList<Error> Errors { get; }
1819

1920
public InvalidModelStateException(ModelStateDictionary modelState, Type resourceType,
20-
bool includeExceptionStackTraceInErrors)
21+
bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy)
2122
{
22-
Errors = FromModelState(modelState, resourceType, includeExceptionStackTraceInErrors);
23+
Errors = FromModelState(modelState, resourceType, includeExceptionStackTraceInErrors, namingStrategy);
2324
}
2425

2526
private static List<Error> FromModelState(ModelStateDictionary modelState, Type resourceType,
26-
bool includeExceptionStackTraceInErrors)
27+
bool includeExceptionStackTraceInErrors, NamingStrategy namingStrategy)
2728
{
2829
List<Error> errors = new List<Error>();
2930

@@ -32,9 +33,8 @@ private static List<Error> FromModelState(ModelStateDictionary modelState, Type
3233
var propertyName = pair.Key;
3334
PropertyInfo property = resourceType.GetProperty(propertyName);
3435

35-
// TODO: Need access to ResourceContext here, in order to determine attribute name when not explicitly set.
3636
string attributeName =
37-
property?.GetCustomAttribute<AttrAttribute>().PublicAttributeName ?? property?.Name;
37+
property.GetCustomAttribute<AttrAttribute>().PublicAttributeName ?? namingStrategy.GetPropertyName(property.Name, false);
3838

3939
foreach (var modelError in pair.Value.Errors)
4040
{

src/JsonApiDotNetCore/Graph/ResourceNameFormatter.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ internal sealed class ResourceNameFormatter
1313

1414
public ResourceNameFormatter(IJsonApiOptions options)
1515
{
16-
var contractResolver = (DefaultContractResolver) options.SerializerSettings.ContractResolver;
17-
_namingStrategy = contractResolver.NamingStrategy;
16+
_namingStrategy = options.SerializerContractResolver.NamingStrategy;
1817
}
1918

2019
/// <summary>

src/JsonApiDotNetCore/Internal/DefaultRoutingConvention.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,7 @@ private string TemplateFromResource(ControllerModel model)
106106
/// </summary>
107107
private string TemplateFromController(ControllerModel model)
108108
{
109-
var contractResolver = (DefaultContractResolver) _options.SerializerSettings.ContractResolver;
110-
string controllerName = contractResolver.NamingStrategy.GetPropertyName(model.ControllerName, false);
109+
string controllerName = _options.SerializerContractResolver.NamingStrategy.GetPropertyName(model.ControllerName, false);
111110

112111
var template = $"{_options.Namespace}/{controllerName}";
113112
if (_registeredTemplates.Add(template))

src/JsonApiDotNetCore/Models/Annotation/AttrAttribute.cs

+45-28
Original file line numberDiff line numberDiff line change
@@ -8,58 +8,75 @@ namespace JsonApiDotNetCore.Models
88
public sealed class AttrAttribute : Attribute, IResourceField
99
{
1010
/// <summary>
11-
/// Defines a public attribute exposed by the API
11+
/// Exposes a resource property as a json:api attribute using the configured casing convention and capabilities.
1212
/// </summary>
13-
///
14-
/// <param name="publicName">How this attribute is exposed through the API</param>
15-
/// <param name="isImmutable">Prevent PATCH requests from updating the value</param>
16-
/// <param name="isFilterable">Prevent filters on this attribute</param>
17-
/// <param name="isSortable">Prevent this attribute from being sorted by</param>
18-
///
1913
/// <example>
20-
///
2114
/// <code>
2215
/// public class Author : Identifiable
2316
/// {
2417
/// [Attr]
2518
/// public string Name { get; set; }
2619
/// }
2720
/// </code>
28-
///
2921
/// </example>
30-
public AttrAttribute(string publicName = null, bool isImmutable = false, bool isFilterable = true, bool isSortable = true)
22+
public AttrAttribute()
3123
{
32-
PublicAttributeName = publicName;
33-
IsImmutable = isImmutable;
34-
IsFilterable = isFilterable;
35-
IsSortable = isSortable;
3624
}
3725

38-
string IResourceField.PropertyName => PropertyInfo.Name;
39-
4026
/// <summary>
41-
/// How this attribute is exposed through the API
27+
/// Exposes a resource property as a json:api attribute with an explicit name, using configured capabilities.
4228
/// </summary>
43-
public string PublicAttributeName { get; internal set; }
29+
public AttrAttribute(string publicName)
30+
{
31+
if (publicName == null)
32+
{
33+
throw new ArgumentNullException(nameof(publicName));
34+
}
35+
36+
if (string.IsNullOrWhiteSpace(publicName))
37+
{
38+
throw new ArgumentException("Exposed name cannot be empty or contain only whitespace.", nameof(publicName));
39+
}
40+
41+
PublicAttributeName = publicName;
42+
}
4443

4544
/// <summary>
46-
/// Prevents PATCH requests from updating the value.
45+
/// Exposes a resource property as a json:api attribute using the configured casing convention and an explicit set of capabilities.
4746
/// </summary>
48-
public bool IsImmutable { get; }
47+
/// <example>
48+
/// <code>
49+
/// public class Author : Identifiable
50+
/// {
51+
/// [Attr(AttrCapabilities.AllowFilter | AttrCapabilities.AllowSort)]
52+
/// public string Name { get; set; }
53+
/// }
54+
/// </code>
55+
/// </example>
56+
public AttrAttribute(AttrCapabilities capabilities)
57+
{
58+
HasExplicitCapabilities = true;
59+
Capabilities = capabilities;
60+
}
4961

5062
/// <summary>
51-
/// Whether or not this attribute can be filtered on via a query string filters.
52-
/// Attempts to filter on an attribute with `IsFilterable == false` will return
53-
/// an HTTP 400 response.
63+
/// Exposes a resource property as a json:api attribute with an explicit name and capabilities.
5464
/// </summary>
55-
public bool IsFilterable { get; }
65+
public AttrAttribute(string publicName, AttrCapabilities capabilities) : this(publicName)
66+
{
67+
HasExplicitCapabilities = true;
68+
Capabilities = capabilities;
69+
}
70+
71+
string IResourceField.PropertyName => PropertyInfo.Name;
5672

5773
/// <summary>
58-
/// Whether or not this attribute can be sorted on via a query string sort.
59-
/// Attempts to filter on an attribute with `IsSortable == false` will return
60-
/// an HTTP 400 response.
74+
/// The publicly exposed name of this json:api attribute.
6175
/// </summary>
62-
public bool IsSortable { get; }
76+
public string PublicAttributeName { get; internal set; }
77+
78+
internal bool HasExplicitCapabilities { get; }
79+
public AttrCapabilities Capabilities { get; internal set; }
6380

6481
/// <summary>
6582
/// The resource property that this attribute is declared on.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System;
2+
3+
namespace JsonApiDotNetCore.Models
4+
{
5+
/// <summary>
6+
/// Indicates query string capabilities that can be performed on an <see cref="AttrAttribute"/>.
7+
/// </summary>
8+
[Flags]
9+
public enum AttrCapabilities
10+
{
11+
None = 0,
12+
13+
/// <summary>
14+
/// Whether or not PATCH requests can update the attribute value.
15+
/// Attempts to update when disabled will return an HTTP 422 response.
16+
/// </summary>
17+
AllowMutate = 1,
18+
19+
/// <summary>
20+
/// Whether or not an attribute can be filtered on via a query string parameter.
21+
/// Attempts to sort when disabled will return an HTTP 400 response.
22+
/// </summary>
23+
AllowFilter = 2,
24+
25+
/// <summary>
26+
/// Whether or not an attribute can be sorted on via a query string parameter.
27+
/// Attempts to sort when disabled will return an HTTP 400 response.
28+
/// </summary>
29+
AllowSort = 4,
30+
31+
All = AllowMutate | AllowFilter | AllowSort
32+
}
33+
}

src/JsonApiDotNetCore/Models/JsonApiDocuments/ErrorMeta.cs

+9-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@ public sealed class ErrorMeta
1515

1616
public void IncludeExceptionStackTrace(Exception exception)
1717
{
18-
Data["StackTrace"] = exception?.Demystify().ToString()
19-
.Split(new[] {"\n"}, int.MaxValue, StringSplitOptions.RemoveEmptyEntries);
18+
if (exception == null)
19+
{
20+
Data.Remove("StackTrace");
21+
}
22+
else
23+
{
24+
Data["StackTrace"] = exception.Demystify().ToString()
25+
.Split(new[] { "\n" }, int.MaxValue, StringSplitOptions.RemoveEmptyEntries);
26+
}
2027
}
2128
}
2229
}

src/JsonApiDotNetCore/QueryParameterServices/FilterService.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ private FilterQueryContext GetQueryContexts(FilterQuery query, string parameterN
6363
queryContext.Relationship = GetRelationship(parameterName, query.Relationship);
6464
var attribute = GetAttribute(parameterName, query.Attribute, queryContext.Relationship);
6565

66-
if (!attribute.IsFilterable)
66+
if (!attribute.Capabilities.HasFlag(AttrCapabilities.AllowFilter))
6767
{
6868
throw new InvalidQueryStringParameterException(parameterName, "Filtering on the requested attribute is not allowed.",
6969
$"Filtering on attribute '{attribute.PublicAttributeName}' is not allowed.");

src/JsonApiDotNetCore/QueryParameterServices/SortService.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using JsonApiDotNetCore.Internal.Contracts;
66
using JsonApiDotNetCore.Internal.Query;
77
using JsonApiDotNetCore.Managers.Contracts;
8+
using JsonApiDotNetCore.Models;
89
using Microsoft.Extensions.Primitives;
910

1011
namespace JsonApiDotNetCore.Query
@@ -90,7 +91,7 @@ private SortQueryContext BuildQueryContext(SortQuery query)
9091
var relationship = GetRelationship("sort", query.Relationship);
9192
var attribute = GetAttribute("sort", query.Attribute, relationship);
9293

93-
if (!attribute.IsSortable)
94+
if (!attribute.Capabilities.HasFlag(AttrCapabilities.AllowSort))
9495
{
9596
throw new InvalidQueryStringParameterException("sort", "Sorting on the requested attribute is not allowed.",
9697
$"Sorting on attribute '{attribute.PublicAttributeName}' is not allowed.");

0 commit comments

Comments
 (0)