diff --git a/Directory.Build.props b/Directory.Build.props
index d55c6ddae7..2ef7f513f1 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -18,4 +18,4 @@
29.0.1
4.13.1
-
\ No newline at end of file
+
diff --git a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs
index b75bacd347..0ff4c7fbbd 100644
--- a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs
+++ b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs
@@ -8,6 +8,7 @@
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Serialization;
using JsonApiDotNetCore.Serialization.Server;
+using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
namespace Benchmarks.Serialization
@@ -38,8 +39,7 @@ public JsonApiDeserializerBenchmarks()
var options = new JsonApiOptions();
IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options);
var targetedFields = new TargetedFields();
-
- _jsonApiDeserializer = new RequestDeserializer(resourceGraph, new DefaultResourceFactory(new ServiceContainer()), targetedFields);
+ _jsonApiDeserializer = new RequestDeserializer(resourceGraph, new DefaultResourceFactory(new ServiceContainer()), targetedFields, new HttpContextAccessor());
}
[Benchmark]
diff --git a/docs/usage/options.md b/docs/usage/options.md
index 5283055728..4f52a93883 100644
--- a/docs/usage/options.md
+++ b/docs/usage/options.md
@@ -88,4 +88,12 @@ If you would like to use ASP.NET Core ModelState validation into your controller
```c#
options.ValidateModelState = true;
```
+You will need to use the JsonApiDotNetCore 'IsRequiredAttribute' instead of the built-in 'RequiredAttribute' because it contains modifications to enable partial patching.
+```c#
+public class Person : Identifiable
+{
+ [IsRequired(AllowEmptyStrings = true)]
+ public string FirstName { get; set; }
+}
+```
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs
index 01b0d1e352..bde8b8f310 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs
@@ -7,6 +7,7 @@ namespace JsonApiDotNetCoreExample.Models
public sealed class Article : Identifiable
{
[Attr]
+ [IsRequired(AllowEmptyStrings = true)]
public string Name { get; set; }
[HasOne]
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs
index 27b817ca9c..7af14d5235 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs
@@ -6,6 +6,7 @@ namespace JsonApiDotNetCoreExample.Models
public sealed class Author : Identifiable
{
[Attr]
+ [IsRequired(AllowEmptyStrings = true)]
public string Name { get; set; }
[HasMany]
diff --git a/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs b/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs
index c6a4a8988c..07bd3b66f4 100644
--- a/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs
+++ b/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs
@@ -14,5 +14,17 @@ internal static void SetJsonApiRequest(this HttpContext httpContext)
{
httpContext.Items["IsJsonApiRequest"] = bool.TrueString;
}
+
+ internal static void DisableValidator(this HttpContext httpContext, string propertyName, string model)
+ {
+ var itemKey = $"JsonApiDotNetCore_DisableValidation_{model}_{propertyName}";
+ httpContext.Items[itemKey] = true;
+ }
+
+ internal static bool IsValidatorDisabled(this HttpContext httpContext, string propertyName, string model)
+ {
+ return httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_{propertyName}") ||
+ httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_Relation");
+ }
}
}
diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
index 9a1e0c0104..e2808125b5 100644
--- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
+++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
@@ -1,4 +1,4 @@
-
+
4.0.0
$(NetCoreAppVersion)
diff --git a/src/JsonApiDotNetCore/Models/IsRequiredAttribute.cs b/src/JsonApiDotNetCore/Models/IsRequiredAttribute.cs
new file mode 100644
index 0000000000..d28c106e09
--- /dev/null
+++ b/src/JsonApiDotNetCore/Models/IsRequiredAttribute.cs
@@ -0,0 +1,24 @@
+using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using JsonApiDotNetCore.Extensions;
+
+namespace JsonApiDotNetCore.Models
+{
+ public sealed class IsRequiredAttribute : RequiredAttribute
+ {
+ private bool _isDisabled;
+
+ public override bool IsValid(object value)
+ {
+ return _isDisabled || base.IsValid(value);
+ }
+
+ protected override ValidationResult IsValid(object value, ValidationContext validationContext)
+ {
+ var httpContextAccessor = (IHttpContextAccessor)validationContext.GetRequiredService(typeof(IHttpContextAccessor));
+ _isDisabled = httpContextAccessor.HttpContext.IsValidatorDisabled(validationContext.MemberName, validationContext.ObjectType.Name);
+ return _isDisabled ? ValidationResult.Success : base.IsValid(value, validationContext);
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs
index b4626bf884..7169686f03 100644
--- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs
+++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs
@@ -69,7 +69,7 @@ protected object Deserialize(string body)
/// Attributes and their values, as in the serialized content
/// Exposed attributes for
///
- protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary attributeValues, List attributes)
+ protected virtual IIdentifiable SetAttributes(IIdentifiable entity, Dictionary attributeValues, List attributes)
{
if (attributeValues == null || attributeValues.Count == 0)
return entity;
@@ -86,6 +86,7 @@ protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary
/// Sets the relationships on a parsed entity
///
@@ -93,7 +94,7 @@ protected IIdentifiable SetAttributes(IIdentifiable entity, DictionaryRelationships and their values, as in the serialized content
/// Exposed relationships for
///
- protected IIdentifiable SetRelationships(IIdentifiable entity, Dictionary relationshipsValues, List relationshipAttributes)
+ protected virtual IIdentifiable SetRelationships(IIdentifiable entity, Dictionary relationshipsValues, List relationshipAttributes)
{
if (relationshipsValues == null || relationshipsValues.Count == 0)
return entity;
@@ -108,7 +109,6 @@ protected IIdentifiable SetRelationships(IIdentifiable entity, Dictionary
@@ -50,5 +56,40 @@ protected override void AfterProcessField(IIdentifiable entity, IResourceField f
else if (field is RelationshipAttribute relationship)
_targetedFields.Relationships.Add(relationship);
}
+
+ protected override IIdentifiable SetAttributes(IIdentifiable entity, Dictionary attributeValues, List attributes)
+ {
+ if (_httpContextAccessor.HttpContext.Request.Method == HttpMethod.Patch.Method)
+ {
+ foreach (AttrAttribute attr in attributes)
+ {
+ if (attr.PropertyInfo.GetCustomAttribute() != null)
+ {
+ bool disableValidator = attributeValues == null || attributeValues.Count == 0 ||
+ !attributeValues.TryGetValue(attr.PublicAttributeName, out _);
+
+ if (disableValidator)
+ {
+ _httpContextAccessor.HttpContext.DisableValidator(attr.PropertyInfo.Name, entity.GetType().Name);
+ }
+ }
+ }
+ }
+
+ return base.SetAttributes(entity, attributeValues, attributes);
+ }
+
+ protected override IIdentifiable SetRelationships(IIdentifiable entity, Dictionary relationshipsValues, List relationshipAttributes)
+ {
+ // If there is a relationship included in the data of the POST or PATCH, then the 'IsRequired' attribute will be disabled for any
+ // property within that object. For instance, a new article is posted and has a relationship included to an author. In this case,
+ // the author name (which has the 'IsRequired' attribute) will not be included in the POST. Unless disabled, the POST will fail.
+ foreach (RelationshipAttribute attr in relationshipAttributes)
+ {
+ _httpContextAccessor.HttpContext.DisableValidator("Relation", attr.PropertyInfo.Name);
+ }
+
+ return base.SetRelationships(entity, relationshipsValues, relationshipAttributes);
+ }
}
}
diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs
index 94cffa66bb..9ca4b55a70 100644
--- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs
+++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs
@@ -21,18 +21,23 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance
[Collection("WebHostCollection")]
public sealed class ManyToManyTests
{
- private readonly Faker _articleFaker = new Faker()
- .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10))
- .RuleFor(a => a.Author, f => new Author());
+ private readonly TestFixture _fixture;
+ private readonly Faker _authorFaker;
+ private readonly Faker _articleFaker;
private readonly Faker _tagFaker;
- private readonly TestFixture _fixture;
-
public ManyToManyTests(TestFixture fixture)
{
_fixture = fixture;
+ _authorFaker = new Faker()
+ .RuleFor(a => a.Name, f => f.Random.Words(2));
+
+ _articleFaker = new Faker()
+ .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10))
+ .RuleFor(a => a.Author, f => _authorFaker.Generate());
+
_tagFaker = new Faker()
.CustomInstantiator(f => new Tag(_fixture.GetService()))
.RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10));
@@ -282,7 +287,7 @@ public async Task Can_Create_Many_To_Many()
// Arrange
var context = _fixture.GetService();
var tag = _tagFaker.Generate();
- var author = new Author();
+ var author = _authorFaker.Generate();
context.Tags.Add(tag);
context.AuthorDifferentDbContextName.Add(author);
await context.SaveChangesAsync();
@@ -294,6 +299,10 @@ public async Task Can_Create_Many_To_Many()
data = new
{
type = "articles",
+ attributes = new Dictionary
+ {
+ {"name", "An article with relationships"}
+ },
relationships = new Dictionary
{
{ "author", new {
diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs
index 7907b28305..20ecf96a5a 100644
--- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs
+++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs
@@ -1,13 +1,16 @@
+using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
+using Bogus;
using JsonApiDotNetCore;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Models.JsonApiDocuments;
using JsonApiDotNetCoreExample.Data;
using JsonApiDotNetCoreExample.Models;
using JsonApiDotNetCoreExampleTests.Acceptance.Spec;
+using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using Xunit;
@@ -15,9 +18,28 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance
{
public sealed class ModelStateValidationTests : FunctionalTestCollection
{
+ private readonly Faker _articleFaker;
+ private readonly Faker _authorFaker;
+ private readonly Faker _tagFaker;
+
public ModelStateValidationTests(StandardApplicationFactory factory)
: base(factory)
{
+ var options = (JsonApiOptions) _factory.GetService();
+ options.ValidateModelState = true;
+
+ var context = _factory.GetService();
+
+ _authorFaker = new Faker()
+ .RuleFor(a => a.Name, f => f.Random.Words(2));
+
+ _articleFaker = new Faker()
+ .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10))
+ .RuleFor(a => a.Author, f => _authorFaker.Generate());
+
+ _tagFaker = new Faker()
+ .CustomInstantiator(f => new Tag(context))
+ .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10));
}
[Fact]
@@ -38,9 +60,6 @@ public async Task When_posting_tag_with_invalid_name_it_must_fail()
};
request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType);
- var options = (JsonApiOptions)_factory.GetService();
- options.ValidateModelState = true;
-
// Act
var response = await _factory.Client.SendAsync(request);
@@ -112,9 +131,6 @@ public async Task When_patching_tag_with_invalid_name_it_must_fail()
};
request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType);
- var options = (JsonApiOptions)_factory.GetService();
- options.ValidateModelState = true;
-
// Act
var response = await _factory.Client.SendAsync(request);
@@ -167,5 +183,378 @@ public async Task When_patching_tag_with_invalid_name_without_model_state_valida
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
+
+ [Fact]
+ public async Task Create_Article_With_IsRequired_Name_Attribute_Succeeds()
+ {
+ // Arrange
+ string name = "Article Title";
+ var context = _factory.GetService();
+ var author = _authorFaker.Generate();
+ context.AuthorDifferentDbContextName.Add(author);
+ await context.SaveChangesAsync();
+
+ var route = "/api/v1/articles";
+ var request = new HttpRequestMessage(HttpMethod.Post, route);
+ var content = new
+ {
+ data = new
+ {
+ type = "articles",
+ attributes = new Dictionary
+ {
+ {"name", name}
+ },
+ relationships = new Dictionary
+ {
+ { "author", new
+ {
+ data = new
+ {
+ type = "authors",
+ id = author.StringId
+ }
+ }
+ }
+ }
+ }
+ };
+ request.Content = new StringContent(JsonConvert.SerializeObject(content));
+ request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType);
+
+ // Act
+ var response = await _factory.Client.SendAsync(request);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.Equal(HttpStatusCode.Created, response.StatusCode);
+
+ var articleResponse = GetDeserializer().DeserializeSingle(body).Data;
+ Assert.NotNull(articleResponse);
+
+ var persistedArticle = await _dbContext.Articles
+ .SingleAsync(a => a.Id == articleResponse.Id);
+
+ Assert.Equal(name, persistedArticle.Name);
+ }
+
+ [Fact]
+ public async Task Create_Article_With_IsRequired_Name_Attribute_Empty_Succeeds()
+ {
+ // Arrange
+ string name = string.Empty;
+ var context = _factory.GetService();
+ var author = _authorFaker.Generate();
+ context.AuthorDifferentDbContextName.Add(author);
+ await context.SaveChangesAsync();
+
+ var route = "/api/v1/articles";
+ var request = new HttpRequestMessage(HttpMethod.Post, route);
+ var content = new
+ {
+ data = new
+ {
+ type = "articles",
+ attributes = new Dictionary
+ {
+ {"name", name}
+ },
+ relationships = new Dictionary
+ {
+ { "author", new
+ {
+ data = new
+ {
+ type = "authors",
+ id = author.StringId
+ }
+ }
+ }
+ }
+ }
+ };
+ request.Content = new StringContent(JsonConvert.SerializeObject(content));
+ request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType);
+
+ // Act
+ var response = await _factory.Client.SendAsync(request);
+
+ // Assert
+ var body = await response.Content.ReadAsStringAsync();
+ Assert.Equal(HttpStatusCode.Created, response.StatusCode);
+
+ var articleResponse = GetDeserializer().DeserializeSingle(body).Data;
+ Assert.NotNull(articleResponse);
+
+ var persistedArticle = await _dbContext.Articles
+ .SingleAsync(a => a.Id == articleResponse.Id);
+
+ Assert.Equal(name, persistedArticle.Name);
+ }
+
+ [Fact]
+ public async Task Create_Article_With_IsRequired_Name_Attribute_Explicitly_Null_Fails()
+ {
+ // Arrange
+ var context = _factory.GetService();
+ var author = _authorFaker.Generate();
+ context.AuthorDifferentDbContextName.Add(author);
+ await context.SaveChangesAsync();
+
+ var route = "/api/v1/articles";
+ var request = new HttpRequestMessage(HttpMethod.Post, route);
+ var content = new
+ {
+ data = new
+ {
+ type = "articles",
+ attributes = new Dictionary
+ {
+ {"name", null}
+ },
+ relationships = new Dictionary
+ {
+ { "author", new
+ {
+ data = new
+ {
+ type = "authors",
+ id = author.StringId
+ }
+ }
+ }
+ }
+ }
+ };
+ request.Content = new StringContent(JsonConvert.SerializeObject(content));
+ request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType);
+
+ // Act
+ var response = await _factory.Client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode);
+ var body = await response.Content.ReadAsStringAsync();
+ var errorDocument = JsonConvert.DeserializeObject(body);
+ Assert.Single(errorDocument.Errors);
+ Assert.Equal("Input validation failed.", errorDocument.Errors[0].Title);
+ Assert.Equal("422", errorDocument.Errors[0].Status);
+ Assert.Equal("The Name field is required.", errorDocument.Errors[0].Detail);
+ }
+
+ [Fact]
+ public async Task Create_Article_With_IsRequired_Name_Attribute_Missing_Fails()
+ {
+ // Arrange
+ var context = _factory.GetService();
+ var author = _authorFaker.Generate();
+ context.AuthorDifferentDbContextName.Add(author);
+ await context.SaveChangesAsync();
+
+ var route = "/api/v1/articles";
+ var request = new HttpRequestMessage(HttpMethod.Post, route);
+ var content = new
+ {
+ data = new
+ {
+ type = "articles",
+ relationships = new Dictionary
+ {
+ { "author", new
+ {
+ data = new
+ {
+ type = "authors",
+ id = author.StringId
+ }
+ }
+ }
+ }
+ }
+ };
+ request.Content = new StringContent(JsonConvert.SerializeObject(content));
+ request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType);
+
+ // Act
+ var response = await _factory.Client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode);
+ var body = await response.Content.ReadAsStringAsync();
+ var errorDocument = JsonConvert.DeserializeObject(body);
+ Assert.Single(errorDocument.Errors);
+ Assert.Equal("Input validation failed.", errorDocument.Errors[0].Title);
+ Assert.Equal("422", errorDocument.Errors[0].Status);
+ Assert.Equal("The Name field is required.", errorDocument.Errors[0].Detail);
+ }
+
+ [Fact]
+ public async Task Update_Article_With_IsRequired_Name_Attribute_Succeeds()
+ {
+ // Arrange
+ var name = "Article Name";
+ var context = _factory.GetService();
+ var article = _articleFaker.Generate();
+ context.Articles.Add(article);
+ await context.SaveChangesAsync();
+
+ var route = $"/api/v1/articles/{article.Id}";
+ var request = new HttpRequestMessage(HttpMethod.Patch, route);
+ var content = new
+ {
+ data = new
+ {
+ type = "articles",
+ id = article.StringId,
+ attributes = new Dictionary
+ {
+ {"name", name}
+ }
+ }
+ };
+
+ request.Content = new StringContent(JsonConvert.SerializeObject(content));
+ request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType);
+
+ // Act
+ var response = await _factory.Client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ var persistedArticle = await _dbContext.Articles
+ .SingleOrDefaultAsync(a => a.Id == article.Id);
+
+ var updatedName = persistedArticle.Name;
+ Assert.Equal(name, updatedName);
+ }
+
+ [Fact]
+ public async Task Update_Article_With_IsRequired_Name_Attribute_Missing_Succeeds()
+ {
+ // Arrange
+ var context = _factory.GetService();
+ var tag = _tagFaker.Generate();
+ var article = _articleFaker.Generate();
+ context.Tags.Add(tag);
+ context.Articles.Add(article);
+ await context.SaveChangesAsync();
+
+ var route = $"/api/v1/articles/{article.Id}";
+ var request = new HttpRequestMessage(HttpMethod.Patch, route);
+ var content = new
+ {
+ data = new
+ {
+ type = "articles",
+ id = article.StringId,
+ relationships = new Dictionary
+ {
+ { "tags", new
+ {
+ data = new []
+ {
+ new
+ {
+ type = "tags",
+ id = tag.StringId
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+
+ request.Content = new StringContent(JsonConvert.SerializeObject(content));
+ request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType);
+
+ // Act
+ var response = await _factory.Client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+ }
+
+ [Fact]
+ public async Task Update_Article_With_IsRequired_Name_Attribute_Explicitly_Null_Fails()
+ {
+ // Arrange
+ var context = _factory.GetService();
+ var article = _articleFaker.Generate();
+ context.Articles.Add(article);
+ await context.SaveChangesAsync();
+
+ var route = $"/api/v1/articles/{article.Id}";
+ var request = new HttpRequestMessage(HttpMethod.Patch, route);
+ var content = new
+ {
+ data = new
+ {
+ type = "articles",
+ id = article.StringId,
+ attributes = new Dictionary
+ {
+ {"name", null}
+ }
+ }
+ };
+
+ request.Content = new StringContent(JsonConvert.SerializeObject(content));
+ request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType);
+
+ // Act
+ var response = await _factory.Client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode);
+
+ var body = await response.Content.ReadAsStringAsync();
+ var errorDocument = JsonConvert.DeserializeObject(body);
+ Assert.Single(errorDocument.Errors);
+ Assert.Equal("Input validation failed.", errorDocument.Errors[0].Title);
+ Assert.Equal("422", errorDocument.Errors[0].Status);
+ Assert.Equal("The Name field is required.", errorDocument.Errors[0].Detail);
+ }
+
+ [Fact]
+ public async Task Update_Article_With_IsRequired_Name_Attribute_Empty_Succeeds()
+ {
+ // Arrange
+ var context = _factory.GetService();
+ var article = _articleFaker.Generate();
+ context.Articles.Add(article);
+ await context.SaveChangesAsync();
+
+ var route = $"/api/v1/articles/{article.Id}";
+ var request = new HttpRequestMessage(HttpMethod.Patch, route);
+ var content = new
+ {
+ data = new
+ {
+ type = "articles",
+ id = article.StringId,
+ attributes = new Dictionary
+ {
+ {"name", ""}
+ }
+ }
+ };
+
+ request.Content = new StringContent(JsonConvert.SerializeObject(content));
+ request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType);
+
+ // Act
+ var response = await _factory.Client.SendAsync(request);
+
+ // Assert
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ var persistedArticle = await _dbContext.Articles
+ .SingleOrDefaultAsync(a => a.Id == article.Id);
+
+ var updatedName = persistedArticle.Name;
+ Assert.Equal("", updatedName);
+ }
}
}
diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs
index 41ea2d9d45..8629e4f4b9 100644
--- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs
+++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs
@@ -22,20 +22,24 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance
public sealed class ResourceDefinitionTests
{
private readonly TestFixture _fixture;
+
private readonly AppDbContext _context;
private readonly Faker _userFaker;
private readonly Faker _todoItemFaker;
private readonly Faker _personFaker;
- private readonly Faker _articleFaker = new Faker()
- .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10))
- .RuleFor(a => a.Author, f => new Author());
-
+ private readonly Faker _articleFaker;
+ private readonly Faker _authorFaker;
private readonly Faker _tagFaker;
public ResourceDefinitionTests(TestFixture fixture)
{
_fixture = fixture;
_context = fixture.GetService();
+ _authorFaker = new Faker()
+ .RuleFor(a => a.Name, f => f.Random.Words(2));
+ _articleFaker = new Faker()
+ .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10))
+ .RuleFor(a => a.Author, f => _authorFaker.Generate());
_userFaker = new Faker()
.CustomInstantiator(f => new User(_context))
.RuleFor(u => u.Username, f => f.Internet.UserName())
diff --git a/test/UnitTests/Models/ResourceConstructionTests.cs b/test/UnitTests/Models/ResourceConstructionTests.cs
index b684491f85..044fe6300f 100644
--- a/test/UnitTests/Models/ResourceConstructionTests.cs
+++ b/test/UnitTests/Models/ResourceConstructionTests.cs
@@ -7,8 +7,10 @@
using JsonApiDotNetCore.Serialization;
using JsonApiDotNetCore.Serialization.Server;
using JsonApiDotNetCoreExample.Data;
+using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
+using Moq;
using Newtonsoft.Json;
using Xunit;
@@ -16,6 +18,14 @@ namespace UnitTests.Models
{
public sealed class ResourceConstructionTests
{
+ public Mock _mockHttpContextAccessor;
+
+ public ResourceConstructionTests()
+ {
+ _mockHttpContextAccessor = new Mock();
+ _mockHttpContextAccessor.Setup(mock => mock.HttpContext).Returns(new DefaultHttpContext());
+ }
+
[Fact]
public void When_resource_has_default_constructor_it_must_succeed()
{
@@ -24,7 +34,7 @@ public void When_resource_has_default_constructor_it_must_succeed()
.AddResource()
.Build();
- var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(new ServiceContainer()), new TargetedFields());
+ var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(new ServiceContainer()), new TargetedFields(), _mockHttpContextAccessor.Object);
var body = new
{
@@ -53,7 +63,7 @@ public void When_resource_has_default_constructor_that_throws_it_must_fail()
.AddResource()
.Build();
- var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(new ServiceContainer()), new TargetedFields());
+ var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(new ServiceContainer()), new TargetedFields(), _mockHttpContextAccessor.Object);
var body = new
{
@@ -89,7 +99,7 @@ public void When_resource_has_constructor_with_injectable_parameter_it_must_succ
var serviceContainer = new ServiceContainer();
serviceContainer.AddService(typeof(AppDbContext), appDbContext);
- var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(serviceContainer), new TargetedFields());
+ var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(serviceContainer), new TargetedFields(), _mockHttpContextAccessor.Object);
var body = new
{
@@ -119,7 +129,7 @@ public void When_resource_has_constructor_with_string_parameter_it_must_fail()
.AddResource()
.Build();
- var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(new ServiceContainer()), new TargetedFields());
+ var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(new ServiceContainer()), new TargetedFields(), _mockHttpContextAccessor.Object);
var body = new
{
diff --git a/test/UnitTests/Serialization/DeserializerTestsSetup.cs b/test/UnitTests/Serialization/DeserializerTestsSetup.cs
index 16c0426b7e..3d35316cf4 100644
--- a/test/UnitTests/Serialization/DeserializerTestsSetup.cs
+++ b/test/UnitTests/Serialization/DeserializerTestsSetup.cs
@@ -1,14 +1,22 @@
-using System;
using JsonApiDotNetCore.Internal.Contracts;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Serialization;
using System.Collections.Generic;
using JsonApiDotNetCore.Internal;
+using Microsoft.AspNetCore.Http;
+using Moq;
namespace UnitTests.Serialization
{
public class DeserializerTestsSetup : SerializationTestsSetupBase
{
+ public Mock _mockHttpContextAccessor;
+
+ public DeserializerTestsSetup()
+ {
+ _mockHttpContextAccessor = new Mock();
+ _mockHttpContextAccessor.Setup(mock => mock.HttpContext).Returns(new DefaultHttpContext());
+ }
protected sealed class TestDocumentParser : BaseDocumentParser
{
public TestDocumentParser(IResourceGraph resourceGraph, IResourceFactory resourceFactory) : base(resourceGraph, resourceFactory) { }
diff --git a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs
index 1094f60b37..9f31c2dc28 100644
--- a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs
+++ b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs
@@ -6,6 +6,7 @@
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Serialization;
using JsonApiDotNetCore.Serialization.Server;
+using Microsoft.AspNetCore.Http;
using Moq;
using Newtonsoft.Json;
using Xunit;
@@ -19,7 +20,7 @@ public sealed class RequestDeserializerTests : DeserializerTestsSetup
private readonly Mock _fieldsManagerMock = new Mock();
public RequestDeserializerTests()
{
- _deserializer = new RequestDeserializer(_resourceGraph, new DefaultResourceFactory(new ServiceContainer()), _fieldsManagerMock.Object);
+ _deserializer = new RequestDeserializer(_resourceGraph, new DefaultResourceFactory(new ServiceContainer()), _fieldsManagerMock.Object, _mockHttpContextAccessor.Object);
}
[Fact]