Skip to content

Commit 423f45f

Browse files
sasman0001Bart Koelman
and
Bart Koelman
authored
JADNC: Required Input validation disabled for partial patching / relationships (#781)
Fixes #472 This feature allows the Required validator to be disabled. This will allow partial patching in the case that the attribute is excluded from a patch. Validation is applied when the attribute is present in post or patch. Required validation also disabled for a relationships attributes (in attempt to fix [#472 (comment)](#472 (comment))). Co-authored-by: Bart Koelman <[email protected]>
1 parent 8831ce2 commit 423f45f

File tree

16 files changed

+539
-31
lines changed

16 files changed

+539
-31
lines changed

Directory.Build.props

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@
1818
<BogusVersion>29.0.1</BogusVersion>
1919
<MoqVersion>4.13.1</MoqVersion>
2020
</PropertyGroup>
21-
</Project>
21+
</Project>

benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using JsonApiDotNetCore.Models;
99
using JsonApiDotNetCore.Serialization;
1010
using JsonApiDotNetCore.Serialization.Server;
11+
using Microsoft.AspNetCore.Http;
1112
using Newtonsoft.Json;
1213

1314
namespace Benchmarks.Serialization
@@ -38,8 +39,7 @@ public JsonApiDeserializerBenchmarks()
3839
var options = new JsonApiOptions();
3940
IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options);
4041
var targetedFields = new TargetedFields();
41-
42-
_jsonApiDeserializer = new RequestDeserializer(resourceGraph, new DefaultResourceFactory(new ServiceContainer()), targetedFields);
42+
_jsonApiDeserializer = new RequestDeserializer(resourceGraph, new DefaultResourceFactory(new ServiceContainer()), targetedFields, new HttpContextAccessor());
4343
}
4444

4545
[Benchmark]

docs/usage/options.md

+8
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,12 @@ If you would like to use ASP.NET Core ModelState validation into your controller
8888
```c#
8989
options.ValidateModelState = true;
9090
```
91+
You will need to use the JsonApiDotNetCore 'IsRequiredAttribute' instead of the built-in 'RequiredAttribute' because it contains modifications to enable partial patching.
9192

93+
```c#
94+
public class Person : Identifiable
95+
{
96+
[IsRequired(AllowEmptyStrings = true)]
97+
public string FirstName { get; set; }
98+
}
99+
```

src/Examples/JsonApiDotNetCoreExample/Models/Article.cs

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace JsonApiDotNetCoreExample.Models
77
public sealed class Article : Identifiable
88
{
99
[Attr]
10+
[IsRequired(AllowEmptyStrings = true)]
1011
public string Name { get; set; }
1112

1213
[HasOne]

src/Examples/JsonApiDotNetCoreExample/Models/Author.cs

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace JsonApiDotNetCoreExample.Models
66
public sealed class Author : Identifiable
77
{
88
[Attr]
9+
[IsRequired(AllowEmptyStrings = true)]
910
public string Name { get; set; }
1011

1112
[HasMany]

src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs

+12
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,17 @@ internal static void SetJsonApiRequest(this HttpContext httpContext)
1414
{
1515
httpContext.Items["IsJsonApiRequest"] = bool.TrueString;
1616
}
17+
18+
internal static void DisableValidator(this HttpContext httpContext, string propertyName, string model)
19+
{
20+
var itemKey = $"JsonApiDotNetCore_DisableValidation_{model}_{propertyName}";
21+
httpContext.Items[itemKey] = true;
22+
}
23+
24+
internal static bool IsValidatorDisabled(this HttpContext httpContext, string propertyName, string model)
25+
{
26+
return httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_{propertyName}") ||
27+
httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_Relation");
28+
}
1729
}
1830
}

src/JsonApiDotNetCore/JsonApiDotNetCore.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
33
<VersionPrefix>4.0.0</VersionPrefix>
44
<TargetFramework>$(NetCoreAppVersion)</TargetFramework>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using JsonApiDotNetCore.Extensions;
5+
6+
namespace JsonApiDotNetCore.Models
7+
{
8+
public sealed class IsRequiredAttribute : RequiredAttribute
9+
{
10+
private bool _isDisabled;
11+
12+
public override bool IsValid(object value)
13+
{
14+
return _isDisabled || base.IsValid(value);
15+
}
16+
17+
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
18+
{
19+
var httpContextAccessor = (IHttpContextAccessor)validationContext.GetRequiredService(typeof(IHttpContextAccessor));
20+
_isDisabled = httpContextAccessor.HttpContext.IsValidatorDisabled(validationContext.MemberName, validationContext.ObjectType.Name);
21+
return _isDisabled ? ValidationResult.Success : base.IsValid(value, validationContext);
22+
}
23+
}
24+
}

src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ protected object Deserialize(string body)
6969
/// <param name="attributeValues">Attributes and their values, as in the serialized content</param>
7070
/// <param name="attributes">Exposed attributes for <paramref name="entity"/></param>
7171
/// <returns></returns>
72-
protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary<string, object> attributeValues, List<AttrAttribute> attributes)
72+
protected virtual IIdentifiable SetAttributes(IIdentifiable entity, Dictionary<string, object> attributeValues, List<AttrAttribute> attributes)
7373
{
7474
if (attributeValues == null || attributeValues.Count == 0)
7575
return entity;
@@ -86,14 +86,15 @@ protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary<string, o
8686

8787
return entity;
8888
}
89+
8990
/// <summary>
9091
/// Sets the relationships on a parsed entity
9192
/// </summary>
9293
/// <param name="entity">The parsed entity</param>
9394
/// <param name="relationshipsValues">Relationships and their values, as in the serialized content</param>
9495
/// <param name="relationshipAttributes">Exposed relationships for <paramref name="entity"/></param>
9596
/// <returns></returns>
96-
protected IIdentifiable SetRelationships(IIdentifiable entity, Dictionary<string, RelationshipEntry> relationshipsValues, List<RelationshipAttribute> relationshipAttributes)
97+
protected virtual IIdentifiable SetRelationships(IIdentifiable entity, Dictionary<string, RelationshipEntry> relationshipsValues, List<RelationshipAttribute> relationshipAttributes)
9798
{
9899
if (relationshipsValues == null || relationshipsValues.Count == 0)
99100
return entity;
@@ -108,7 +109,6 @@ protected IIdentifiable SetRelationships(IIdentifiable entity, Dictionary<string
108109
SetHasOneRelationship(entity, entityProperties, hasOneAttribute, relationshipData);
109110
else
110111
SetHasManyRelationship(entity, (HasManyAttribute)attr, relationshipData);
111-
112112
}
113113
return entity;
114114
}

src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs

+43-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
using System;
21
using JsonApiDotNetCore.Exceptions;
32
using JsonApiDotNetCore.Internal;
43
using JsonApiDotNetCore.Internal.Contracts;
54
using JsonApiDotNetCore.Models;
5+
using Microsoft.AspNetCore.Http;
6+
using System.Collections.Generic;
7+
using System.Reflection;
8+
using JsonApiDotNetCore.Extensions;
9+
using System.Net.Http;
610

711
namespace JsonApiDotNetCore.Serialization.Server
812
{
@@ -12,11 +16,13 @@ namespace JsonApiDotNetCore.Serialization.Server
1216
public class RequestDeserializer : BaseDocumentParser, IJsonApiDeserializer
1317
{
1418
private readonly ITargetedFields _targetedFields;
19+
private readonly IHttpContextAccessor _httpContextAccessor;
1520

16-
public RequestDeserializer(IResourceContextProvider contextProvider, IResourceFactory resourceFactory, ITargetedFields targetedFields)
21+
public RequestDeserializer(IResourceContextProvider contextProvider, IResourceFactory resourceFactory, ITargetedFields targetedFields, IHttpContextAccessor httpContextAccessor)
1722
: base(contextProvider, resourceFactory)
1823
{
1924
_targetedFields = targetedFields;
25+
_httpContextAccessor = httpContextAccessor;
2026
}
2127

2228
/// <inheritdoc/>
@@ -50,5 +56,40 @@ protected override void AfterProcessField(IIdentifiable entity, IResourceField f
5056
else if (field is RelationshipAttribute relationship)
5157
_targetedFields.Relationships.Add(relationship);
5258
}
59+
60+
protected override IIdentifiable SetAttributes(IIdentifiable entity, Dictionary<string, object> attributeValues, List<AttrAttribute> attributes)
61+
{
62+
if (_httpContextAccessor.HttpContext.Request.Method == HttpMethod.Patch.Method)
63+
{
64+
foreach (AttrAttribute attr in attributes)
65+
{
66+
if (attr.PropertyInfo.GetCustomAttribute<IsRequiredAttribute>() != null)
67+
{
68+
bool disableValidator = attributeValues == null || attributeValues.Count == 0 ||
69+
!attributeValues.TryGetValue(attr.PublicAttributeName, out _);
70+
71+
if (disableValidator)
72+
{
73+
_httpContextAccessor.HttpContext.DisableValidator(attr.PropertyInfo.Name, entity.GetType().Name);
74+
}
75+
}
76+
}
77+
}
78+
79+
return base.SetAttributes(entity, attributeValues, attributes);
80+
}
81+
82+
protected override IIdentifiable SetRelationships(IIdentifiable entity, Dictionary<string, RelationshipEntry> relationshipsValues, List<RelationshipAttribute> relationshipAttributes)
83+
{
84+
// If there is a relationship included in the data of the POST or PATCH, then the 'IsRequired' attribute will be disabled for any
85+
// property within that object. For instance, a new article is posted and has a relationship included to an author. In this case,
86+
// the author name (which has the 'IsRequired' attribute) will not be included in the POST. Unless disabled, the POST will fail.
87+
foreach (RelationshipAttribute attr in relationshipAttributes)
88+
{
89+
_httpContextAccessor.HttpContext.DisableValidator("Relation", attr.PropertyInfo.Name);
90+
}
91+
92+
return base.SetRelationships(entity, relationshipsValues, relationshipAttributes);
93+
}
5394
}
5495
}

test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs

+15-6
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,23 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance
2121
[Collection("WebHostCollection")]
2222
public sealed class ManyToManyTests
2323
{
24-
private readonly Faker<Article> _articleFaker = new Faker<Article>()
25-
.RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10))
26-
.RuleFor(a => a.Author, f => new Author());
24+
private readonly TestFixture<TestStartup> _fixture;
2725

26+
private readonly Faker<Author> _authorFaker;
27+
private readonly Faker<Article> _articleFaker;
2828
private readonly Faker<Tag> _tagFaker;
2929

30-
private readonly TestFixture<TestStartup> _fixture;
31-
3230
public ManyToManyTests(TestFixture<TestStartup> fixture)
3331
{
3432
_fixture = fixture;
3533

34+
_authorFaker = new Faker<Author>()
35+
.RuleFor(a => a.Name, f => f.Random.Words(2));
36+
37+
_articleFaker = new Faker<Article>()
38+
.RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10))
39+
.RuleFor(a => a.Author, f => _authorFaker.Generate());
40+
3641
_tagFaker = new Faker<Tag>()
3742
.CustomInstantiator(f => new Tag(_fixture.GetService<AppDbContext>()))
3843
.RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10));
@@ -282,7 +287,7 @@ public async Task Can_Create_Many_To_Many()
282287
// Arrange
283288
var context = _fixture.GetService<AppDbContext>();
284289
var tag = _tagFaker.Generate();
285-
var author = new Author();
290+
var author = _authorFaker.Generate();
286291
context.Tags.Add(tag);
287292
context.AuthorDifferentDbContextName.Add(author);
288293
await context.SaveChangesAsync();
@@ -294,6 +299,10 @@ public async Task Can_Create_Many_To_Many()
294299
data = new
295300
{
296301
type = "articles",
302+
attributes = new Dictionary<string, object>
303+
{
304+
{"name", "An article with relationships"}
305+
},
297306
relationships = new Dictionary<string, dynamic>
298307
{
299308
{ "author", new {

0 commit comments

Comments
 (0)