From c4c27aa7cb840d9f53a545e3dc538f4c22087696 Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Tue, 9 Jun 2020 11:31:42 -0600 Subject: [PATCH 01/26] disable validator if partial patch --- .../Models/Article.cs | 4 +- .../Controllers/BaseJsonApiController.cs | 2 +- .../Formatters/JsonApiReader.cs | 1 + .../JsonApiDotNetCore.csproj | 1 + .../CustomValidators/RequiredIfEnabled.cs | 43 ++ .../Common/BaseDocumentParser.cs | 61 ++- .../Server/Contracts/IJsonApiDeserializer.cs | 2 + .../Server/RequestDeserializer.cs | 3 +- .../Acceptance/ManyToManyTests.cs | 4 + .../ResourceDefinitionTests.cs | 375 ++++++++++++++++++ 10 files changed, 482 insertions(+), 14 deletions(-) create mode 100644 src/JsonApiDotNetCore/Models/CustomValidators/RequiredIfEnabled.cs diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs index 01b0d1e352..a752a5c648 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs @@ -1,15 +1,17 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.CustomValidators; namespace JsonApiDotNetCoreExample.Models { public sealed class Article : Identifiable { [Attr] + [RequiredIfEnabled(AllowEmptyStrings = true)] public string Name { get; set; } - [HasOne] + [HasOne] public Author Author { get; set; } public int AuthorId { get; set; } diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index e9464fb269..1f55446dd6 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -132,7 +132,7 @@ public virtual async Task PatchAsync(TId id, [FromBody] T entity) if (_update == null) throw new RequestMethodNotAllowedException(HttpMethod.Patch); if (entity == null) throw new InvalidRequestBodyException(null, null, null); - + if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid) { var namingStrategy = _jsonApiOptions.SerializerContractResolver.NamingStrategy; diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs index 3cab898881..72d00afb31 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs @@ -47,6 +47,7 @@ public async Task ReadAsync(InputFormatterContext context) object model; try { + _deserializer.Context = context; model = _deserializer.Deserialize(body); } catch (InvalidRequestBodyException exception) diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 9a1e0c0104..cf35e28dbb 100644 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -23,6 +23,7 @@ + diff --git a/src/JsonApiDotNetCore/Models/CustomValidators/RequiredIfEnabled.cs b/src/JsonApiDotNetCore/Models/CustomValidators/RequiredIfEnabled.cs new file mode 100644 index 0000000000..ff7c8687da --- /dev/null +++ b/src/JsonApiDotNetCore/Models/CustomValidators/RequiredIfEnabled.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using System.ComponentModel.DataAnnotations; + +namespace JsonApiDotNetCore.Models.CustomValidators +{ + public class RequiredIfEnabled : RequiredAttribute + { + public bool Disabled { get; set; } + + public override bool IsValid(object value) + { + if (Disabled) + { + return true; + } + + return base.IsValid(value); + } + + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var itemKey = this.CreateKey(validationContext.ObjectType.Name, validationContext.MemberName); + var httpContextAccessor = (IHttpContextAccessor)validationContext.GetRequiredService(typeof(IHttpContextAccessor)); + if (httpContextAccessor.HttpContext.Items.ContainsKey(itemKey)) + { + Disabled = true; + } + + if (Disabled) + { + return ValidationResult.Success; + } + + return base.IsValid(value, validationContext); + } + + private string CreateKey(string model, string propertyName) + { + return string.Format("DisableValidation_{0}_{1}", model, propertyName); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs index b4626bf884..a93ed41d79 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -1,4 +1,5 @@ using System; +using System.Web.Http.Validation.Validators; using System.Collections.Generic; using System.IO; using System.Linq; @@ -12,6 +13,8 @@ using JsonApiDotNetCore.Serialization.Server; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using JsonApiDotNetCore.Models.CustomValidators; +using Microsoft.AspNetCore.Mvc.Formatters; namespace JsonApiDotNetCore.Serialization { @@ -19,10 +22,12 @@ namespace JsonApiDotNetCore.Serialization /// Abstract base class for deserialization. Deserializes JSON content into s /// And constructs instances of the resource(s) in the document body. /// - public abstract class BaseDocumentParser + public abstract class BaseDocumentParser : IJsonApiDeserializer { + public InputFormatterContext Context { get; set; } + protected readonly IResourceContextProvider _contextProvider; - protected readonly IResourceFactory _resourceFactory; + protected readonly IResourceFactory _resourceFactory; protected Document _document; protected BaseDocumentParser(IResourceContextProvider contextProvider, IResourceFactory resourceFactory) @@ -46,7 +51,7 @@ protected BaseDocumentParser(IResourceContextProvider contextProvider, IResource protected abstract void AfterProcessField(IIdentifiable entity, IResourceField field, RelationshipEntry data = null); /// - protected object Deserialize(string body) + public object Deserialize(string body) { var bodyJToken = LoadJToken(body); _document = bodyJToken.ToObject(); @@ -71,21 +76,57 @@ protected object Deserialize(string body) /// protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary attributeValues, List attributes) { - if (attributeValues == null || attributeValues.Count == 0) - return entity; - foreach (var attr in attributes) { - if (attributeValues.TryGetValue(attr.PublicAttributeName, out object newValue)) + if (attributeValues == null || attributeValues.Count == 0) + { + if (Context.HttpContext.Request.Method == "PATCH") + { + var requiredAttribute = attr.PropertyInfo.GetCustomAttribute(); + if (requiredAttribute != null) + { + var itemKey = this.CreateKey(attr.PropertyInfo.ReflectedType.Name, attr.PropertyInfo.Name); + if (!Context.HttpContext.Items.ContainsKey(itemKey)) + { + Context.HttpContext.Items.Add(itemKey, true); + } + } + } + } + else { - var convertedValue = ConvertAttrValue(newValue, attr.PropertyInfo.PropertyType); - attr.SetValue(entity, convertedValue); - AfterProcessField(entity, attr); + if (attributeValues.TryGetValue(attr.PublicAttributeName, out object newValue)) + { + var convertedValue = ConvertAttrValue(newValue, attr.PropertyInfo.PropertyType); + attr.SetValue(entity, convertedValue); + AfterProcessField(entity, attr); + } + else + { + if (Context.HttpContext.Request.Method == "PATCH") + { + var requiredAttribute = attr.PropertyInfo.GetCustomAttribute(); + if (requiredAttribute != null) + { + var itemKey = this.CreateKey(attr.PropertyInfo.ReflectedType.Name, attr.PropertyInfo.Name); + if (!Context.HttpContext.Items.ContainsKey(itemKey)) + { + Context.HttpContext.Items.Add(itemKey, true); + } + } + } + } } } return entity; } + + private string CreateKey(string model, string propertyName) + { + return string.Format("DisableValidation_{0}_{1}", model, propertyName); + } + /// /// Sets the relationships on a parsed entity /// diff --git a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiDeserializer.cs index 680391a755..85ea469191 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/Contracts/IJsonApiDeserializer.cs @@ -1,4 +1,5 @@ using JsonApiDotNetCore.Models; +using Microsoft.AspNetCore.Mvc.Formatters; namespace JsonApiDotNetCore.Serialization.Server { @@ -7,6 +8,7 @@ namespace JsonApiDotNetCore.Serialization.Server /// public interface IJsonApiDeserializer { + public InputFormatterContext Context { get; set; } /// /// Deserializes JSON in to a and constructs entities /// from . diff --git a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs index c17ac03473..4346aaedf6 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs @@ -1,4 +1,3 @@ -using System; using JsonApiDotNetCore.Exceptions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; @@ -9,7 +8,7 @@ namespace JsonApiDotNetCore.Serialization.Server /// /// Server deserializer implementation of the /// - public class RequestDeserializer : BaseDocumentParser, IJsonApiDeserializer + public class RequestDeserializer : BaseDocumentParser { private readonly ITargetedFields _targetedFields; diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs index 94cffa66bb..b41a47501b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs @@ -294,6 +294,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/ResourceDefinitions/ResourceDefinitionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs index 41ea2d9d45..a9bc968bc7 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs @@ -609,5 +609,380 @@ public async Task Cascade_Permission_Error_Delete_ToMany_Relationship() Assert.Equal("You are not allowed to update fields or relationships of locked todo items.", errorDocument.Errors[0].Title); Assert.Null(errorDocument.Errors[0].Detail); } + + [Fact] + public async Task Create_Article_With_RequiredOnPost_Name_Attribute_Succeeds() + { + // Arrange + string name = "Article Title"; + var context = _fixture.GetService(); + var author = new Author(); + context.AuthorDifferentDbContextName.Add(author); + await context.SaveChangesAsync(); + + var route = "/api/v1/articles"; + var request = new HttpRequestMessage(new 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 _fixture.Client.SendAsync(request); + + // Assert + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + var articleResponse = _fixture.GetDeserializer().DeserializeSingle
(body).Data; + Assert.NotNull(articleResponse); + + var persistedArticle = await _fixture.Context.Articles + .SingleAsync(a => a.Id == articleResponse.Id); + + Assert.Equal(name, persistedArticle.Name); + } + + [Fact] + public async Task Create_Article_With_RequiredOnPost_Name_Attribute_Empty_Succeeds() + { + // Arrange + string name = string.Empty; + var context = _fixture.GetService(); + var author = new Author(); + context.AuthorDifferentDbContextName.Add(author); + await context.SaveChangesAsync(); + + var route = "/api/v1/articles"; + var request = new HttpRequestMessage(new 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 _fixture.Client.SendAsync(request); + + // Assert + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + var articleResponse = _fixture.GetDeserializer().DeserializeSingle
(body).Data; + Assert.NotNull(articleResponse); + + var persistedArticle = await _fixture.Context.Articles + .SingleAsync(a => a.Id == articleResponse.Id); + + Assert.Equal(name, persistedArticle.Name); + } + + [Fact] + public async Task Create_Article_With_Required_Name_Attribute_Explicitly_Null_Fails() + { + // Arrange + var context = _fixture.GetService(); + var author = new Author(); + context.AuthorDifferentDbContextName.Add(author); + await context.SaveChangesAsync(); + + var route = "/api/v1/articles"; + var request = new HttpRequestMessage(new 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 _fixture.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_RequiredOnPost_Name_Attribute_Missing_Fails() + { + // Arrange + var context = _fixture.GetService(); + var author = new Author(); + context.AuthorDifferentDbContextName.Add(author); + await context.SaveChangesAsync(); + + var route = "/api/v1/articles"; + var request = new HttpRequestMessage(new 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 _fixture.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_Required_Name_Attribute_Succeeds() + { + // Arrange + var name = "Article Name"; + var context = _fixture.GetService(); + var article = _articleFaker.Generate(); + context.Articles.Add(article); + await context.SaveChangesAsync(); + + var route = $"/api/v1/articles/{article.Id}"; + var request = new HttpRequestMessage(new 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 _fixture.Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + _fixture.ReloadDbContext(); + var persistedArticle = await _fixture.Context.Articles + .SingleOrDefaultAsync(a => a.Id == article.Id); + + var updatedName = persistedArticle.Name; + Assert.Equal(name, updatedName); + } + + [Fact] + public async Task Update_Article_With_Required_Name_Attribute_Missing_Succeeds() + { + // Arrange + var context = _fixture.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(new 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 _fixture.Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task Update_Article_With_Required_Name_Attribute_Explicitly_Null_Fails() + { + // Arrange + var context = _fixture.GetService(); + var article = _articleFaker.Generate(); + context.Articles.Add(article); + await context.SaveChangesAsync(); + + var route = $"/api/v1/articles/{article.Id}"; + var request = new HttpRequestMessage(new 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 _fixture.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_Required_AllowEmptyString_True_Name_Attribute_Empty_Succeeds() + { + // Arrange + var context = _fixture.GetService(); + var article = _articleFaker.Generate(); + context.Articles.Add(article); + await context.SaveChangesAsync(); + + var route = $"/api/v1/articles/{article.Id}"; + var request = new HttpRequestMessage(new 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 _fixture.Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + _fixture.ReloadDbContext(); + var persistedArticle = await _fixture.Context.Articles + .SingleOrDefaultAsync(a => a.Id == article.Id); + + var updatedName = persistedArticle.Name; + Assert.Equal("", updatedName); + } } } From ead6930572c6ba17770b2704dd79dae8c0ba9416 Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Tue, 9 Jun 2020 16:45:32 -0600 Subject: [PATCH 02/26] Create required validator that can be diabled to allow for partial patching and relationships. --- .../Models/Article.cs | 2 +- .../JsonApiDotNetCoreExample/Models/Author.cs | 2 + .../ReportsExample/ReportsExample.csproj | 3 +- .../CustomValidators/RequiredIfEnabled.cs | 32 ++++--------- .../Common/BaseDocumentParser.cs | 47 ++++++++----------- .../ResourceDefinitionTests.cs | 6 +-- 6 files changed, 36 insertions(+), 56 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs index a752a5c648..cae364b716 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCoreExample.Models public sealed class Article : Identifiable { [Attr] - [RequiredIfEnabled(AllowEmptyStrings = true)] + [Required(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..57bf3493a7 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs @@ -1,11 +1,13 @@ using JsonApiDotNetCore.Models; using System.Collections.Generic; +using JsonApiDotNetCore.Models.CustomValidators; namespace JsonApiDotNetCoreExample.Models { public sealed class Author : Identifiable { [Attr] + [Required(AllowEmptyStrings = true)] public string Name { get; set; } [HasMany] diff --git a/src/Examples/ReportsExample/ReportsExample.csproj b/src/Examples/ReportsExample/ReportsExample.csproj index e1b25693a5..9d3c6301a8 100644 --- a/src/Examples/ReportsExample/ReportsExample.csproj +++ b/src/Examples/ReportsExample/ReportsExample.csproj @@ -8,6 +8,7 @@ - + + diff --git a/src/JsonApiDotNetCore/Models/CustomValidators/RequiredIfEnabled.cs b/src/JsonApiDotNetCore/Models/CustomValidators/RequiredIfEnabled.cs index ff7c8687da..2ee1c0a3fd 100644 --- a/src/JsonApiDotNetCore/Models/CustomValidators/RequiredIfEnabled.cs +++ b/src/JsonApiDotNetCore/Models/CustomValidators/RequiredIfEnabled.cs @@ -4,40 +4,26 @@ namespace JsonApiDotNetCore.Models.CustomValidators { - public class RequiredIfEnabled : RequiredAttribute + public class Required : RequiredAttribute { - public bool Disabled { get; set; } + private bool Disabled { get; set; } public override bool IsValid(object value) { - if (Disabled) - { - return true; - } - - return base.IsValid(value); + return Disabled || base.IsValid(value); } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { - var itemKey = this.CreateKey(validationContext.ObjectType.Name, validationContext.MemberName); - var httpContextAccessor = (IHttpContextAccessor)validationContext.GetRequiredService(typeof(IHttpContextAccessor)); - if (httpContextAccessor.HttpContext.Items.ContainsKey(itemKey)) - { - Disabled = true; - } - - if (Disabled) - { - return ValidationResult.Success; - } - - return base.IsValid(value, validationContext); + CheckDisableKey(validationContext); + return Disabled ? ValidationResult.Success : base.IsValid(value, validationContext); } - private string CreateKey(string model, string propertyName) + private void CheckDisableKey(ValidationContext validationContext) { - return string.Format("DisableValidation_{0}_{1}", model, propertyName); + var httpContextAccessor = (IHttpContextAccessor)validationContext.GetRequiredService(typeof(IHttpContextAccessor)); + Disabled = httpContextAccessor.HttpContext.Items.ContainsKey($"DisableValidation_{validationContext.ObjectType.Name}_{validationContext.MemberName}") + || httpContextAccessor.HttpContext.Items.ContainsKey($"DisableValidation_{validationContext.ObjectType.Name}_Relation"); } } } diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs index a93ed41d79..f3b75b7093 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -1,5 +1,4 @@ using System; -using System.Web.Http.Validation.Validators; using System.Collections.Generic; using System.IO; using System.Linq; @@ -13,8 +12,8 @@ using JsonApiDotNetCore.Serialization.Server; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using JsonApiDotNetCore.Models.CustomValidators; using Microsoft.AspNetCore.Mvc.Formatters; +using Required = JsonApiDotNetCore.Models.CustomValidators.Required; namespace JsonApiDotNetCore.Serialization { @@ -80,17 +79,10 @@ protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary() != null) { - var requiredAttribute = attr.PropertyInfo.GetCustomAttribute(); - if (requiredAttribute != null) - { - var itemKey = this.CreateKey(attr.PropertyInfo.ReflectedType.Name, attr.PropertyInfo.Name); - if (!Context.HttpContext.Items.ContainsKey(itemKey)) - { - Context.HttpContext.Items.Add(itemKey, true); - } - } + DisableValidator(attr.PropertyInfo.ReflectedType?.Name, attr.PropertyInfo.Name); } } else @@ -103,17 +95,10 @@ protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary() != null) { - var requiredAttribute = attr.PropertyInfo.GetCustomAttribute(); - if (requiredAttribute != null) - { - var itemKey = this.CreateKey(attr.PropertyInfo.ReflectedType.Name, attr.PropertyInfo.Name); - if (!Context.HttpContext.Items.ContainsKey(itemKey)) - { - Context.HttpContext.Items.Add(itemKey, true); - } - } + DisableValidator(attr.PropertyInfo.ReflectedType?.Name, attr.PropertyInfo.Name); } } } @@ -122,11 +107,6 @@ protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary /// Sets the relationships on a parsed entity ///
@@ -142,6 +122,8 @@ protected IIdentifiable SetRelationships(IIdentifiable entity, Dictionary(); From a4c688d05cc671b48d3d5924a341067355baa6b2 Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Tue, 9 Jun 2020 16:55:36 -0600 Subject: [PATCH 03/26] formatting --- src/Examples/JsonApiDotNetCoreExample/Models/Article.cs | 2 +- src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs | 2 +- .../Serialization/Common/BaseDocumentParser.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs index cae364b716..5208724b63 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs @@ -11,7 +11,7 @@ public sealed class Article : Identifiable [Required(AllowEmptyStrings = true)] public string Name { get; set; } - [HasOne] + [HasOne] public Author Author { get; set; } public int AuthorId { get; set; } diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs index 1f55446dd6..e9464fb269 100644 --- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs +++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs @@ -132,7 +132,7 @@ public virtual async Task PatchAsync(TId id, [FromBody] T entity) if (_update == null) throw new RequestMethodNotAllowedException(HttpMethod.Patch); if (entity == null) throw new InvalidRequestBodyException(null, null, null); - + if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid) { var namingStrategy = _jsonApiOptions.SerializerContractResolver.NamingStrategy; diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs index f3b75b7093..645a87a27f 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -26,7 +26,7 @@ public abstract class BaseDocumentParser : IJsonApiDeserializer public InputFormatterContext Context { get; set; } protected readonly IResourceContextProvider _contextProvider; - protected readonly IResourceFactory _resourceFactory; + protected readonly IResourceFactory _resourceFactory; protected Document _document; protected BaseDocumentParser(IResourceContextProvider contextProvider, IResourceFactory resourceFactory) From 7d3f57ac11941d8ed21dea56053dd12cde4beb01 Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Tue, 9 Jun 2020 17:04:25 -0600 Subject: [PATCH 04/26] Remove unneeded reference. --- src/JsonApiDotNetCore/JsonApiDotNetCore.csproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index cf35e28dbb..e2808125b5 100644 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,4 +1,4 @@ - + 4.0.0 $(NetCoreAppVersion) @@ -23,7 +23,6 @@ - From 11bd7d93e45f3933638597a61abb238112d95708 Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Tue, 9 Jun 2020 17:10:11 -0600 Subject: [PATCH 05/26] package reference --- .../JsonApiDotNetCoreExampleTests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj index 574e486669..32dec7bc26 100644 --- a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj +++ b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj @@ -16,6 +16,7 @@ + From c02ef3492edcc9ba1f0202a36580a930e972ad5d Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Wed, 10 Jun 2020 13:02:18 -0600 Subject: [PATCH 06/26] Requested changes: -HttpContextAccessor injection -Renaming -Moved HttpContext Keys to extensions --- .../JsonApiDeserializerBenchmarks.cs | 5 ++- .../Models/Article.cs | 2 +- .../JsonApiDotNetCoreExample/Models/Author.cs | 2 +- .../ReportsExample/ReportsExample.csproj | 2 +- .../Extensions/HttpContextExtensions.cs | 15 +++++++ .../Formatters/JsonApiReader.cs | 1 - .../CustomValidators/IsRequiredAttribute.cs | 24 ++++++++++++ .../CustomValidators/RequiredIfEnabled.cs | 29 -------------- .../Client/ResponseDeserializer.cs | 3 +- .../Common/BaseDocumentParser.cs | 39 +++++++------------ .../Server/Contracts/IJsonApiDeserializer.cs | 2 - .../Server/RequestDeserializer.cs | 7 ++-- .../Acceptance/ManyToManyTests.cs | 7 +++- .../ResourceDefinitionTests.cs | 29 +++++++------- .../Spec/DeeplyNestedInclusionTests.cs | 2 +- .../Acceptance/Spec/EndToEndTest.cs | 4 +- .../Acceptance/TestFixture.cs | 7 ++-- .../JsonApiDotNetCoreExampleTests.csproj | 1 - .../Models/ResourceConstructionTests.cs | 9 +++-- .../Client/ResponseDeserializerTests.cs | 3 +- .../Common/DocumentParserTests.cs | 3 +- .../Serialization/DeserializerTestsSetup.cs | 4 +- .../Server/RequestDeserializerTests.cs | 3 +- 23 files changed, 108 insertions(+), 95 deletions(-) create mode 100644 src/JsonApiDotNetCore/Models/CustomValidators/IsRequiredAttribute.cs delete mode 100644 src/JsonApiDotNetCore/Models/CustomValidators/RequiredIfEnabled.cs diff --git a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs index b75bacd347..6102fc8be7 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,8 @@ 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/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs index 5208724b63..d9d5824b12 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCoreExample.Models public sealed class Article : Identifiable { [Attr] - [Required(AllowEmptyStrings = true)] + [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 57bf3493a7..a3ab102065 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs @@ -7,7 +7,7 @@ namespace JsonApiDotNetCoreExample.Models public sealed class Author : Identifiable { [Attr] - [Required(AllowEmptyStrings = true)] + [IsRequired(AllowEmptyStrings = true)] public string Name { get; set; } [HasMany] diff --git a/src/Examples/ReportsExample/ReportsExample.csproj b/src/Examples/ReportsExample/ReportsExample.csproj index 9d3c6301a8..e217c035be 100644 --- a/src/Examples/ReportsExample/ReportsExample.csproj +++ b/src/Examples/ReportsExample/ReportsExample.csproj @@ -9,6 +9,6 @@ - + diff --git a/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs b/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs index c6a4a8988c..40fa68e49a 100644 --- a/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs @@ -14,5 +14,20 @@ internal static void SetJsonApiRequest(this HttpContext httpContext) { httpContext.Items["IsJsonApiRequest"] = bool.TrueString; } + + internal static void DisableValidator(this HttpContext httpContext, string model, string name) + { + if (httpContext == null) return; + var itemKey = $"JsonApiDotNetCore_DisableValidation_{model}_{name}"; + if (!httpContext.Items.ContainsKey(itemKey)) + { + httpContext.Items.Add(itemKey, true); + } + } + internal static bool IsValidatorDisabled(this HttpContext httpContext, string model, string name) + { + return httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_{name}") || + httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_Relation"); + } } } diff --git a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs index 72d00afb31..3cab898881 100644 --- a/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs +++ b/src/JsonApiDotNetCore/Formatters/JsonApiReader.cs @@ -47,7 +47,6 @@ public async Task ReadAsync(InputFormatterContext context) object model; try { - _deserializer.Context = context; model = _deserializer.Deserialize(body); } catch (InvalidRequestBodyException exception) diff --git a/src/JsonApiDotNetCore/Models/CustomValidators/IsRequiredAttribute.cs b/src/JsonApiDotNetCore/Models/CustomValidators/IsRequiredAttribute.cs new file mode 100644 index 0000000000..cb4756e5d3 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/CustomValidators/IsRequiredAttribute.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using System.ComponentModel.DataAnnotations; +using JsonApiDotNetCore.Extensions; + +namespace JsonApiDotNetCore.Models.CustomValidators +{ + public 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.ObjectType.Name, validationContext.MemberName); + return _isDisabled ? ValidationResult.Success : base.IsValid(value, validationContext); + } + } +} diff --git a/src/JsonApiDotNetCore/Models/CustomValidators/RequiredIfEnabled.cs b/src/JsonApiDotNetCore/Models/CustomValidators/RequiredIfEnabled.cs deleted file mode 100644 index 2ee1c0a3fd..0000000000 --- a/src/JsonApiDotNetCore/Models/CustomValidators/RequiredIfEnabled.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using System.ComponentModel.DataAnnotations; - -namespace JsonApiDotNetCore.Models.CustomValidators -{ - public class Required : RequiredAttribute - { - private bool Disabled { get; set; } - - public override bool IsValid(object value) - { - return Disabled || base.IsValid(value); - } - - protected override ValidationResult IsValid(object value, ValidationContext validationContext) - { - CheckDisableKey(validationContext); - return Disabled ? ValidationResult.Success : base.IsValid(value, validationContext); - } - - private void CheckDisableKey(ValidationContext validationContext) - { - var httpContextAccessor = (IHttpContextAccessor)validationContext.GetRequiredService(typeof(IHttpContextAccessor)); - Disabled = httpContextAccessor.HttpContext.Items.ContainsKey($"DisableValidation_{validationContext.ObjectType.Name}_{validationContext.MemberName}") - || httpContextAccessor.HttpContext.Items.ContainsKey($"DisableValidation_{validationContext.ObjectType.Name}_Relation"); - } - } -} diff --git a/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs index c3210eb131..4417444280 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs @@ -5,6 +5,7 @@ using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; +using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.Serialization.Client { @@ -13,7 +14,7 @@ namespace JsonApiDotNetCore.Serialization.Client /// public class ResponseDeserializer : BaseDocumentParser, IResponseDeserializer { - public ResponseDeserializer(IResourceContextProvider contextProvider, IResourceFactory resourceFactory) : base(contextProvider, resourceFactory) { } + public ResponseDeserializer(IResourceContextProvider contextProvider, IResourceFactory resourceFactory, IHttpContextAccessor httpContextAccessor) : base(contextProvider, resourceFactory, httpContextAccessor) { } /// public DeserializedSingleResponse DeserializeSingle(string body) where TResource : class, IIdentifiable diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs index 645a87a27f..1ef946ac60 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -8,12 +8,12 @@ using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; +using JsonApiDotNetCore.Models.CustomValidators; using JsonApiDotNetCore.Serialization.Client; using JsonApiDotNetCore.Serialization.Server; +using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Microsoft.AspNetCore.Mvc.Formatters; -using Required = JsonApiDotNetCore.Models.CustomValidators.Required; namespace JsonApiDotNetCore.Serialization { @@ -21,18 +21,19 @@ namespace JsonApiDotNetCore.Serialization /// Abstract base class for deserialization. Deserializes JSON content into s /// And constructs instances of the resource(s) in the document body. /// - public abstract class BaseDocumentParser : IJsonApiDeserializer + public abstract class BaseDocumentParser { - public InputFormatterContext Context { get; set; } - protected readonly IResourceContextProvider _contextProvider; protected readonly IResourceFactory _resourceFactory; + protected readonly IHttpContextAccessor _httpContextAccessor; + protected Document _document; - protected BaseDocumentParser(IResourceContextProvider contextProvider, IResourceFactory resourceFactory) + protected BaseDocumentParser(IResourceContextProvider contextProvider, IResourceFactory resourceFactory, IHttpContextAccessor httpContextAccessor) { _contextProvider = contextProvider; _resourceFactory = resourceFactory; + _httpContextAccessor = httpContextAccessor; } /// @@ -50,7 +51,7 @@ protected BaseDocumentParser(IResourceContextProvider contextProvider, IResource protected abstract void AfterProcessField(IIdentifiable entity, IResourceField field, RelationshipEntry data = null); /// - public object Deserialize(string body) + protected object Deserialize(string body) { var bodyJToken = LoadJToken(body); _document = bodyJToken.ToObject(); @@ -79,10 +80,10 @@ protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary() != null) + if (_httpContextAccessor?.HttpContext?.Request.Method != "PATCH") continue; + if (attr.PropertyInfo.GetCustomAttribute() != null) { - DisableValidator(attr.PropertyInfo.ReflectedType?.Name, attr.PropertyInfo.Name); + _httpContextAccessor.HttpContext.DisableValidator(attr.PropertyInfo.ReflectedType?.Name, attr.PropertyInfo.Name); } } else @@ -95,10 +96,10 @@ protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary() != null) + if (_httpContextAccessor?.HttpContext?.Request.Method != "PATCH") continue; + if (attr.PropertyInfo.GetCustomAttribute() != null) { - DisableValidator(attr.PropertyInfo.ReflectedType?.Name, attr.PropertyInfo.Name); + _httpContextAccessor.HttpContext.DisableValidator(attr.PropertyInfo.ReflectedType?.Name, attr.PropertyInfo.Name); } } } @@ -122,7 +123,7 @@ protected IIdentifiable SetRelationships(IIdentifiable entity, Dictionary public interface IJsonApiDeserializer { - public InputFormatterContext Context { get; set; } /// /// Deserializes JSON in to a and constructs entities /// from . diff --git a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs index 4346aaedf6..e05bb07a18 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs @@ -2,18 +2,19 @@ using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; +using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.Serialization.Server { /// /// Server deserializer implementation of the /// - public class RequestDeserializer : BaseDocumentParser + public class RequestDeserializer : BaseDocumentParser, IJsonApiDeserializer { private readonly ITargetedFields _targetedFields; - public RequestDeserializer(IResourceContextProvider contextProvider, IResourceFactory resourceFactory, ITargetedFields targetedFields) - : base(contextProvider, resourceFactory) + public RequestDeserializer(IResourceContextProvider contextProvider, IResourceFactory resourceFactory, ITargetedFields targetedFields, IHttpContextAccessor httpContextAccessor) + : base(contextProvider, resourceFactory, httpContextAccessor) { _targetedFields = targetedFields; } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs index b41a47501b..06e4ceca73 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs @@ -21,9 +21,12 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance [Collection("WebHostCollection")] public sealed class ManyToManyTests { + private readonly Faker _authorFaker = new Faker() + .RuleFor(a => a.Name, f => f.Random.Words(2)); + private readonly Faker
_articleFaker = new Faker
() .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10)) - .RuleFor(a => a.Author, f => new Author()); + .RuleFor(a => a.Author, f => new Author() { Name = "John Doe"}); private readonly Faker _tagFaker; @@ -282,7 +285,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(); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs index 89b21712c4..91be90f3c4 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs @@ -28,7 +28,10 @@ public sealed class ResourceDefinitionTests 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()); + .RuleFor(a => a.Author, f => new Author() { Name = "John Doe"}); + + private readonly Faker _authorFaker = new Faker() + .RuleFor(a => a.Name, f => f.Random.Words(2)); private readonly Faker _tagFaker; @@ -611,12 +614,12 @@ public async Task Cascade_Permission_Error_Delete_ToMany_Relationship() } [Fact] - public async Task Create_Article_With_Required_Name_Attribute_Succeeds() + public async Task Create_Article_With_IsRequired_Name_Attribute_Succeeds() { // Arrange string name = "Article Title"; var context = _fixture.GetService(); - var author = new Author(); + var author = _authorFaker.Generate(); context.AuthorDifferentDbContextName.Add(author); await context.SaveChangesAsync(); @@ -665,12 +668,12 @@ public async Task Create_Article_With_Required_Name_Attribute_Succeeds() } [Fact] - public async Task Create_Article_With_Required_Name_Attribute_Empty_Succeeds() + public async Task Create_Article_With_IsRequired_Name_Attribute_Empty_Succeeds() { // Arrange string name = string.Empty; var context = _fixture.GetService(); - var author = new Author(); + var author = _authorFaker.Generate(); context.AuthorDifferentDbContextName.Add(author); await context.SaveChangesAsync(); @@ -719,11 +722,11 @@ public async Task Create_Article_With_Required_Name_Attribute_Empty_Succeeds() } [Fact] - public async Task Create_Article_With_Required_Name_Attribute_Explicitly_Null_Fails() + public async Task Create_Article_With_IsRequired_Name_Attribute_Explicitly_Null_Fails() { // Arrange var context = _fixture.GetService(); - var author = new Author(); + var author = _authorFaker.Generate(); context.AuthorDifferentDbContextName.Add(author); await context.SaveChangesAsync(); @@ -769,11 +772,11 @@ public async Task Create_Article_With_Required_Name_Attribute_Explicitly_Null_Fa } [Fact] - public async Task Create_Article_With_Required_Name_Attribute_Missing_Fails() + public async Task Create_Article_With_IsRequired_Name_Attribute_Missing_Fails() { // Arrange var context = _fixture.GetService(); - var author = new Author(); + var author = _authorFaker.Generate(); context.AuthorDifferentDbContextName.Add(author); await context.SaveChangesAsync(); @@ -815,7 +818,7 @@ public async Task Create_Article_With_Required_Name_Attribute_Missing_Fails() } [Fact] - public async Task Update_Article_With_Required_Name_Attribute_Succeeds() + public async Task Update_Article_With_IsRequired_Name_Attribute_Succeeds() { // Arrange var name = "Article Name"; @@ -857,7 +860,7 @@ public async Task Update_Article_With_Required_Name_Attribute_Succeeds() } [Fact] - public async Task Update_Article_With_Required_Name_Attribute_Missing_Succeeds() + public async Task Update_Article_With_IsRequired_Name_Attribute_Missing_Succeeds() { // Arrange var context = _fixture.GetService(); @@ -904,7 +907,7 @@ public async Task Update_Article_With_Required_Name_Attribute_Missing_Succeeds() } [Fact] - public async Task Update_Article_With_Required_Name_Attribute_Explicitly_Null_Fails() + public async Task Update_Article_With_IsRequired_Name_Attribute_Explicitly_Null_Fails() { // Arrange var context = _fixture.GetService(); @@ -945,7 +948,7 @@ public async Task Update_Article_With_Required_Name_Attribute_Explicitly_Null_Fa } [Fact] - public async Task Update_Article_With_Required_AllowEmptyString_True_Name_Attribute_Empty_Succeeds() + public async Task Update_Article_With_IsRequired_Name_Attribute_Empty_Succeeds() { // Arrange var context = _fixture.GetService(); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs index 2a52ebe6d4..457700df89 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs @@ -49,7 +49,7 @@ public async Task Can_Include_Nested_Relationships() .AddResource() .AddResource() .Build(); - var deserializer = new ResponseDeserializer(resourceGraph, new DefaultResourceFactory(_fixture.ServiceProvider)); + var deserializer = new ResponseDeserializer(resourceGraph, new DefaultResourceFactory(_fixture.ServiceProvider), _fixture.HttpContextAccessor); var todoItem = new TodoItem { Collection = new TodoItemCollection diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs index ad99a3da18..ad26a30d30 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs @@ -17,6 +17,7 @@ using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCoreExampleTests.Helpers.Models; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Logging.Abstractions; using Xunit; @@ -71,6 +72,7 @@ protected IRequestSerializer GetSerializer(Expression(); var options = GetService(); var formatter = new ResourceNameFormatter(options); var resourcesContexts = GetService().GetResourceContexts(); @@ -85,7 +87,7 @@ protected IResponseDeserializer GetDeserializer() } builder.AddResource(formatter.FormatResourceName(typeof(TodoItem))); builder.AddResource(formatter.FormatResourceName(typeof(TodoItemCollection))); - return new ResponseDeserializer(builder.Build(), new DefaultResourceFactory(_factory.ServiceProvider)); + return new ResponseDeserializer(builder.Build(), new DefaultResourceFactory(_factory.ServiceProvider), httpContextAccessor); } protected AppDbContext GetDbContext() => GetService(); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs index d6dd9fd675..d4e3e008f6 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs @@ -14,6 +14,7 @@ using JsonApiDotNetCoreExampleTests.Helpers.Models; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -31,13 +32,14 @@ public TestFixture() var builder = new WebHostBuilder().UseStartup(); _server = new TestServer(builder); ServiceProvider = _server.Host.Services; - + HttpContextAccessor = GetService(); Client = _server.CreateClient(); Context = GetService().GetContext() as AppDbContext; } public HttpClient Client { get; set; } public AppDbContext Context { get; private set; } + public HttpContextAccessor HttpContextAccessor { get; set; } public static IRequestSerializer GetSerializer(IServiceProvider serviceProvider, Expression> attributes = null, Expression> relationships = null) where TResource : class, IIdentifiable { @@ -60,7 +62,6 @@ public IRequestSerializer GetSerializer(Expression(); - var resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance) .AddResource() .AddResource
() @@ -73,7 +74,7 @@ public IResponseDeserializer GetDeserializer() .AddResource() .AddResource("todoItems") .AddResource().Build(); - return new ResponseDeserializer(resourceGraph, new DefaultResourceFactory(ServiceProvider)); + return new ResponseDeserializer(resourceGraph, new DefaultResourceFactory(ServiceProvider), HttpContextAccessor); } public T GetService() => (T)ServiceProvider.GetService(typeof(T)); diff --git a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj index 32dec7bc26..574e486669 100644 --- a/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj +++ b/test/JsonApiDotNetCoreExampleTests/JsonApiDotNetCoreExampleTests.csproj @@ -16,7 +16,6 @@ - diff --git a/test/UnitTests/Models/ResourceConstructionTests.cs b/test/UnitTests/Models/ResourceConstructionTests.cs index b684491f85..5c56460e0d 100644 --- a/test/UnitTests/Models/ResourceConstructionTests.cs +++ b/test/UnitTests/Models/ResourceConstructionTests.cs @@ -7,6 +7,7 @@ using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Server; using JsonApiDotNetCoreExample.Data; +using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json; @@ -24,7 +25,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(), new HttpContextAccessor()); var body = new { @@ -53,7 +54,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(), new HttpContextAccessor()); var body = new { @@ -89,7 +90,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(), new HttpContextAccessor()); var body = new { @@ -119,7 +120,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(), new HttpContextAccessor()); var body = new { diff --git a/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs b/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs index 7c93a01da9..c1eba49d72 100644 --- a/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs +++ b/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs @@ -5,6 +5,7 @@ using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.Links; using JsonApiDotNetCore.Serialization.Client; +using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Xunit; using UnitTests.TestModels; @@ -18,7 +19,7 @@ public sealed class ResponseDeserializerTests : DeserializerTestsSetup public ResponseDeserializerTests() { - _deserializer = new ResponseDeserializer(_resourceGraph, new DefaultResourceFactory(new ServiceContainer())); + _deserializer = new ResponseDeserializer(_resourceGraph, new DefaultResourceFactory(new ServiceContainer()), new HttpContextAccessor()); _linkValues.Add("self", "http://example.com/articles"); _linkValues.Add("next", "http://example.com/articles?page[offset]=2"); _linkValues.Add("last", "http://example.com/articles?page[offset]=10"); diff --git a/test/UnitTests/Serialization/Common/DocumentParserTests.cs b/test/UnitTests/Serialization/Common/DocumentParserTests.cs index c6911cb1f5..90947d9e80 100644 --- a/test/UnitTests/Serialization/Common/DocumentParserTests.cs +++ b/test/UnitTests/Serialization/Common/DocumentParserTests.cs @@ -5,6 +5,7 @@ using System.Linq; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; +using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Xunit; using UnitTests.TestModels; @@ -17,7 +18,7 @@ public sealed class BaseDocumentParserTests : DeserializerTestsSetup public BaseDocumentParserTests() { - _deserializer = new TestDocumentParser(_resourceGraph, new DefaultResourceFactory(new ServiceContainer())); + _deserializer = new TestDocumentParser(_resourceGraph, new DefaultResourceFactory(new ServiceContainer()), new HttpContextAccessor()); } [Fact] diff --git a/test/UnitTests/Serialization/DeserializerTestsSetup.cs b/test/UnitTests/Serialization/DeserializerTestsSetup.cs index 16c0426b7e..6fcd022a25 100644 --- a/test/UnitTests/Serialization/DeserializerTestsSetup.cs +++ b/test/UnitTests/Serialization/DeserializerTestsSetup.cs @@ -1,9 +1,9 @@ -using System; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; using System.Collections.Generic; using JsonApiDotNetCore.Internal; +using Microsoft.AspNetCore.Http; namespace UnitTests.Serialization { @@ -11,7 +11,7 @@ public class DeserializerTestsSetup : SerializationTestsSetupBase { protected sealed class TestDocumentParser : BaseDocumentParser { - public TestDocumentParser(IResourceGraph resourceGraph, IResourceFactory resourceFactory) : base(resourceGraph, resourceFactory) { } + public TestDocumentParser(IResourceGraph resourceGraph, IResourceFactory resourceFactory, IHttpContextAccessor httpContextAccessor) : base(resourceGraph, resourceFactory, httpContextAccessor) { } public new object Deserialize(string body) { diff --git a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs index 1094f60b37..07edb5e25e 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, new HttpContextAccessor()); } [Fact] From 9e40b88c86d937221e6aace8dcaf758e3a709e3a Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Wed, 10 Jun 2020 13:26:00 -0600 Subject: [PATCH 07/26] Add version wildcard --- Directory.Build.props | 3 ++- benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs | 1 - src/Examples/ReportsExample/ReportsExample.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index d55c6ddae7..059315ce31 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,6 +5,7 @@ 3.1.* 3.1.* 3.1.* + 3.1.* @@ -18,4 +19,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 6102fc8be7..0ff4c7fbbd 100644 --- a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs @@ -39,7 +39,6 @@ public JsonApiDeserializerBenchmarks() var options = new JsonApiOptions(); IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options); var targetedFields = new TargetedFields(); - _jsonApiDeserializer = new RequestDeserializer(resourceGraph, new DefaultResourceFactory(new ServiceContainer()), targetedFields, new HttpContextAccessor()); } diff --git a/src/Examples/ReportsExample/ReportsExample.csproj b/src/Examples/ReportsExample/ReportsExample.csproj index e217c035be..33629471ce 100644 --- a/src/Examples/ReportsExample/ReportsExample.csproj +++ b/src/Examples/ReportsExample/ReportsExample.csproj @@ -8,7 +8,7 @@ - + From a0de349fe4476f3066e8052af4e8761885bd7c8e Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Wed, 10 Jun 2020 13:49:05 -0600 Subject: [PATCH 08/26] expect possible null in HttpContextExtension method --- .../Extensions/HttpContextExtensions.cs | 6 ++--- .../Common/BaseDocumentParser.cs | 22 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs b/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs index 40fa68e49a..970867e7a3 100644 --- a/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs @@ -15,11 +15,11 @@ internal static void SetJsonApiRequest(this HttpContext httpContext) httpContext.Items["IsJsonApiRequest"] = bool.TrueString; } - internal static void DisableValidator(this HttpContext httpContext, string model, string name) + internal static void DisableValidator(this HttpContext httpContext, string propertyName, string model = null) { if (httpContext == null) return; - var itemKey = $"JsonApiDotNetCore_DisableValidation_{model}_{name}"; - if (!httpContext.Items.ContainsKey(itemKey)) + var itemKey = $"JsonApiDotNetCore_DisableValidation_{model}_{propertyName}"; + if (!httpContext.Items.ContainsKey(itemKey) && model != null) { httpContext.Items.Add(itemKey, true); } diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs index 1ef946ac60..c6ca0e9b57 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -78,13 +78,10 @@ protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary() != null) - { - _httpContextAccessor.HttpContext.DisableValidator(attr.PropertyInfo.ReflectedType?.Name, attr.PropertyInfo.Name); - } + disableValidator = true; } else { @@ -96,13 +93,16 @@ protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary() != null) - { - _httpContextAccessor.HttpContext.DisableValidator(attr.PropertyInfo.ReflectedType?.Name, attr.PropertyInfo.Name); - } + disableValidator = true; } } + + if (!disableValidator) continue; + if (_httpContextAccessor?.HttpContext?.Request.Method != "PATCH") continue; + if (attr.PropertyInfo.GetCustomAttribute() != null) + { + _httpContextAccessor.HttpContext.DisableValidator(attr.PropertyInfo.Name, attr.PropertyInfo.ReflectedType?.Name); + } } return entity; @@ -123,7 +123,7 @@ protected IIdentifiable SetRelationships(IIdentifiable entity, Dictionary Date: Wed, 10 Jun 2020 13:54:11 -0600 Subject: [PATCH 09/26] change parameter order and naming for consistency --- src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs | 4 ++-- .../Models/CustomValidators/IsRequiredAttribute.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs b/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs index 970867e7a3..2730bf92cd 100644 --- a/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs @@ -24,9 +24,9 @@ internal static void DisableValidator(this HttpContext httpContext, string prope httpContext.Items.Add(itemKey, true); } } - internal static bool IsValidatorDisabled(this HttpContext httpContext, string model, string name) + internal static bool IsValidatorDisabled(this HttpContext httpContext, string propertyName, string model) { - return httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_{name}") || + return httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_{propertyName}") || httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_Relation"); } } diff --git a/src/JsonApiDotNetCore/Models/CustomValidators/IsRequiredAttribute.cs b/src/JsonApiDotNetCore/Models/CustomValidators/IsRequiredAttribute.cs index cb4756e5d3..8ea2c7f4a4 100644 --- a/src/JsonApiDotNetCore/Models/CustomValidators/IsRequiredAttribute.cs +++ b/src/JsonApiDotNetCore/Models/CustomValidators/IsRequiredAttribute.cs @@ -17,7 +17,7 @@ public override bool IsValid(object value) protected override ValidationResult IsValid(object value, ValidationContext validationContext) { var httpContextAccessor = (IHttpContextAccessor)validationContext.GetRequiredService(typeof(IHttpContextAccessor)); - _isDisabled = httpContextAccessor.HttpContext.IsValidatorDisabled(validationContext.ObjectType.Name, validationContext.MemberName); + _isDisabled = httpContextAccessor.HttpContext.IsValidatorDisabled(validationContext.MemberName, validationContext.ObjectType.Name); return _isDisabled ? ValidationResult.Success : base.IsValid(value, validationContext); } } From 50ae2e4270db9bca2418aa19561af03ee538e95b Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Wed, 10 Jun 2020 14:40:51 -0600 Subject: [PATCH 10/26] Requested changes --- Directory.Build.props | 1 - src/Examples/ReportsExample/ReportsExample.csproj | 2 +- .../Extensions/HttpContextExtensions.cs | 9 +++------ .../Serialization/Common/BaseDocumentParser.cs | 6 ++++-- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 059315ce31..2ef7f513f1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,6 @@ 3.1.* 3.1.* 3.1.* - 3.1.* diff --git a/src/Examples/ReportsExample/ReportsExample.csproj b/src/Examples/ReportsExample/ReportsExample.csproj index 33629471ce..534bcd3d7a 100644 --- a/src/Examples/ReportsExample/ReportsExample.csproj +++ b/src/Examples/ReportsExample/ReportsExample.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs b/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs index 2730bf92cd..07bd3b66f4 100644 --- a/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs @@ -15,15 +15,12 @@ internal static void SetJsonApiRequest(this HttpContext httpContext) httpContext.Items["IsJsonApiRequest"] = bool.TrueString; } - internal static void DisableValidator(this HttpContext httpContext, string propertyName, string model = null) + internal static void DisableValidator(this HttpContext httpContext, string propertyName, string model) { - if (httpContext == null) return; var itemKey = $"JsonApiDotNetCore_DisableValidation_{model}_{propertyName}"; - if (!httpContext.Items.ContainsKey(itemKey) && model != null) - { - httpContext.Items.Add(itemKey, true); - } + httpContext.Items[itemKey] = true; } + internal static bool IsValidatorDisabled(this HttpContext httpContext, string propertyName, string model) { return httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_{propertyName}") || diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs index c6ca0e9b57..ebc730676b 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; using System.Reflection; using JsonApiDotNetCore.Exceptions; using JsonApiDotNetCore.Extensions; @@ -98,10 +99,11 @@ protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary() != null) { - _httpContextAccessor.HttpContext.DisableValidator(attr.PropertyInfo.Name, attr.PropertyInfo.ReflectedType?.Name); + _httpContextAccessor?.HttpContext.DisableValidator(attr.PropertyInfo.Name, + entity.GetType().Name); } } From 102e636c0d010367f8de8120c02caf55d3a8480b Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Wed, 10 Jun 2020 14:49:44 -0600 Subject: [PATCH 11/26] Remove unneeded null check. --- .../Serialization/Common/BaseDocumentParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs index ebc730676b..869e0b5c21 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -125,7 +125,7 @@ protected IIdentifiable SetRelationships(IIdentifiable entity, Dictionary Date: Wed, 10 Jun 2020 14:55:25 -0600 Subject: [PATCH 12/26] add null checks --- .../Serialization/Common/BaseDocumentParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs index 869e0b5c21..ebc730676b 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -125,7 +125,7 @@ protected IIdentifiable SetRelationships(IIdentifiable entity, Dictionary Date: Wed, 10 Jun 2020 15:07:27 -0600 Subject: [PATCH 13/26] generate fake name for author --- .../Acceptance/ManyToManyTests.cs | 18 ++++++++++-------- .../ResourceDefinitionTests.cs | 15 ++++++++------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs index 06e4ceca73..9ca4b55a70 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs @@ -21,21 +21,23 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance [Collection("WebHostCollection")] public sealed class ManyToManyTests { - private readonly Faker _authorFaker = new Faker() - .RuleFor(a => a.Name, f => f.Random.Words(2)); - - private readonly Faker
_articleFaker = new Faker
() - .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10)) - .RuleFor(a => a.Author, f => new Author() { Name = "John Doe"}); + 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)); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs index 91be90f3c4..f66a1b56bf 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs @@ -22,23 +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() { Name = "John Doe"}); - - private readonly Faker _authorFaker = new Faker() - .RuleFor(a => a.Name, f => f.Random.Words(2)); - + 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()) From 0bcebe35abcb1b1465882c935aa18bdb7e1fd4a9 Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Wed, 10 Jun 2020 15:46:59 -0600 Subject: [PATCH 14/26] Moved logic to RequestDeserializer, overriding methods in BaseDocumentParser --- .../Client/ResponseDeserializer.cs | 2 +- .../Common/BaseDocumentParser.cs | 46 ++++--------- .../Server/RequestDeserializer.cs | 65 ++++++++++++++++++- .../Spec/DeeplyNestedInclusionTests.cs | 2 +- .../Acceptance/Spec/EndToEndTest.cs | 2 +- .../Acceptance/TestFixture.cs | 4 +- .../Client/ResponseDeserializerTests.cs | 2 +- .../Common/DocumentParserTests.cs | 2 +- .../Serialization/DeserializerTestsSetup.cs | 2 +- 9 files changed, 82 insertions(+), 45 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs index 4417444280..47cc7a1099 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs @@ -14,7 +14,7 @@ namespace JsonApiDotNetCore.Serialization.Client ///
public class ResponseDeserializer : BaseDocumentParser, IResponseDeserializer { - public ResponseDeserializer(IResourceContextProvider contextProvider, IResourceFactory resourceFactory, IHttpContextAccessor httpContextAccessor) : base(contextProvider, resourceFactory, httpContextAccessor) { } + public ResponseDeserializer(IResourceContextProvider contextProvider, IResourceFactory resourceFactory) : base(contextProvider, resourceFactory) { } /// public DeserializedSingleResponse DeserializeSingle(string body) where TResource : class, IIdentifiable diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs index ebc730676b..37fcb76d34 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -2,17 +2,14 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net.Http; using System.Reflection; using JsonApiDotNetCore.Exceptions; using JsonApiDotNetCore.Extensions; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.CustomValidators; using JsonApiDotNetCore.Serialization.Client; using JsonApiDotNetCore.Serialization.Server; -using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -26,15 +23,13 @@ public abstract class BaseDocumentParser { protected readonly IResourceContextProvider _contextProvider; protected readonly IResourceFactory _resourceFactory; - protected readonly IHttpContextAccessor _httpContextAccessor; protected Document _document; - protected BaseDocumentParser(IResourceContextProvider contextProvider, IResourceFactory resourceFactory, IHttpContextAccessor httpContextAccessor) + protected BaseDocumentParser(IResourceContextProvider contextProvider, IResourceFactory resourceFactory) { _contextProvider = contextProvider; _resourceFactory = resourceFactory; - _httpContextAccessor = httpContextAccessor; } /// @@ -75,35 +70,18 @@ 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; + foreach (var attr in attributes) - { - var disableValidator = false; - if (attributeValues == null || attributeValues.Count == 0) - { - disableValidator = true; - } - else + { + if (attributeValues.TryGetValue(attr.PublicAttributeName, out object newValue)) { - if (attributeValues.TryGetValue(attr.PublicAttributeName, out object newValue)) - { var convertedValue = ConvertAttrValue(newValue, attr.PropertyInfo.PropertyType); attr.SetValue(entity, convertedValue); AfterProcessField(entity, attr); - } - else - { - disableValidator = true; - } - } - - if (!disableValidator) continue; - if (_httpContextAccessor?.HttpContext?.Request.Method != HttpMethod.Patch.Method) continue; - if (attr.PropertyInfo.GetCustomAttribute() != null) - { - _httpContextAccessor?.HttpContext.DisableValidator(attr.PropertyInfo.Name, - entity.GetType().Name); } } @@ -117,7 +95,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; @@ -125,8 +103,6 @@ protected IIdentifiable SetRelationships(IIdentifiable entity, Dictionary /// /// - private void SetHasOneRelationship(IIdentifiable entity, + protected void SetHasOneRelationship(IIdentifiable entity, PropertyInfo[] entityProperties, HasOneAttribute attr, RelationshipEntry relationshipData) @@ -249,7 +225,7 @@ private void SetNavigation(IIdentifiable entity, HasOneAttribute attr, string re /// /// Sets a HasMany relationship. /// - private void SetHasManyRelationship( + protected void SetHasManyRelationship( IIdentifiable entity, HasManyAttribute attr, RelationshipEntry relationshipData) @@ -270,7 +246,7 @@ private void SetHasManyRelationship( AfterProcessField(entity, attr, relationshipData); } - private object ConvertAttrValue(object newValue, Type targetType) + protected object ConvertAttrValue(object newValue, Type targetType) { if (newValue is JContainer jObject) // the attribute value is a complex type that needs additional deserialization diff --git a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs index e05bb07a18..5626cdba7d 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs @@ -3,6 +3,11 @@ using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using System.Reflection; +using JsonApiDotNetCore.Extensions; +using JsonApiDotNetCore.Models.CustomValidators; +using System.Net.Http; namespace JsonApiDotNetCore.Serialization.Server { @@ -12,11 +17,13 @@ namespace JsonApiDotNetCore.Serialization.Server public class RequestDeserializer : BaseDocumentParser, IJsonApiDeserializer { private readonly ITargetedFields _targetedFields; + private readonly IHttpContextAccessor _httpContextAccessor; public RequestDeserializer(IResourceContextProvider contextProvider, IResourceFactory resourceFactory, ITargetedFields targetedFields, IHttpContextAccessor httpContextAccessor) - : base(contextProvider, resourceFactory, httpContextAccessor) + : base(contextProvider, resourceFactory) { _targetedFields = targetedFields; + _httpContextAccessor = httpContextAccessor; } /// @@ -50,5 +57,61 @@ 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) + { + foreach (var attr in attributes) + { + var disableValidator = false; + if (attributeValues == null || attributeValues.Count == 0) + { + disableValidator = true; + } + else + { + if (attributeValues.TryGetValue(attr.PublicAttributeName, out object newValue)) + { + var convertedValue = ConvertAttrValue(newValue, attr.PropertyInfo.PropertyType); + attr.SetValue(entity, convertedValue); + AfterProcessField(entity, attr); + } + else + { + disableValidator = true; + } + } + + if (!disableValidator) continue; + if (_httpContextAccessor?.HttpContext?.Request.Method != HttpMethod.Patch.Method) continue; + if (attr.PropertyInfo.GetCustomAttribute() != null) + { + _httpContextAccessor?.HttpContext.DisableValidator(attr.PropertyInfo.Name, + entity.GetType().Name); + } + } + + return entity; + } + + protected override IIdentifiable SetRelationships(IIdentifiable entity, Dictionary relationshipsValues, List relationshipAttributes) + { + if (relationshipsValues == null || relationshipsValues.Count == 0) + return entity; + + var entityProperties = entity.GetType().GetProperties(); + foreach (var attr in relationshipAttributes) + { + _httpContextAccessor?.HttpContext?.DisableValidator("Relation", attr.PropertyInfo.Name); + + if (!relationshipsValues.TryGetValue(attr.PublicRelationshipName, out RelationshipEntry relationshipData) || !relationshipData.IsPopulated) + continue; + + if (attr is HasOneAttribute hasOneAttribute) + SetHasOneRelationship(entity, entityProperties, hasOneAttribute, relationshipData); + else + SetHasManyRelationship(entity, (HasManyAttribute)attr, relationshipData); + } + return entity; + } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs index 457700df89..2a52ebe6d4 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/DeeplyNestedInclusionTests.cs @@ -49,7 +49,7 @@ public async Task Can_Include_Nested_Relationships() .AddResource() .AddResource() .Build(); - var deserializer = new ResponseDeserializer(resourceGraph, new DefaultResourceFactory(_fixture.ServiceProvider), _fixture.HttpContextAccessor); + var deserializer = new ResponseDeserializer(resourceGraph, new DefaultResourceFactory(_fixture.ServiceProvider)); var todoItem = new TodoItem { Collection = new TodoItemCollection diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs index ad26a30d30..992a39837b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs @@ -87,7 +87,7 @@ protected IResponseDeserializer GetDeserializer() } builder.AddResource(formatter.FormatResourceName(typeof(TodoItem))); builder.AddResource(formatter.FormatResourceName(typeof(TodoItemCollection))); - return new ResponseDeserializer(builder.Build(), new DefaultResourceFactory(_factory.ServiceProvider), httpContextAccessor); + return new ResponseDeserializer(builder.Build(), new DefaultResourceFactory(_factory.ServiceProvider)); } protected AppDbContext GetDbContext() => GetService(); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs index d4e3e008f6..163b3145a4 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs @@ -32,14 +32,12 @@ public TestFixture() var builder = new WebHostBuilder().UseStartup(); _server = new TestServer(builder); ServiceProvider = _server.Host.Services; - HttpContextAccessor = GetService(); Client = _server.CreateClient(); Context = GetService().GetContext() as AppDbContext; } public HttpClient Client { get; set; } public AppDbContext Context { get; private set; } - public HttpContextAccessor HttpContextAccessor { get; set; } public static IRequestSerializer GetSerializer(IServiceProvider serviceProvider, Expression> attributes = null, Expression> relationships = null) where TResource : class, IIdentifiable { @@ -74,7 +72,7 @@ public IResponseDeserializer GetDeserializer() .AddResource() .AddResource("todoItems") .AddResource().Build(); - return new ResponseDeserializer(resourceGraph, new DefaultResourceFactory(ServiceProvider), HttpContextAccessor); + return new ResponseDeserializer(resourceGraph, new DefaultResourceFactory(ServiceProvider)); } public T GetService() => (T)ServiceProvider.GetService(typeof(T)); diff --git a/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs b/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs index c1eba49d72..1107bf649d 100644 --- a/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs +++ b/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs @@ -19,7 +19,7 @@ public sealed class ResponseDeserializerTests : DeserializerTestsSetup public ResponseDeserializerTests() { - _deserializer = new ResponseDeserializer(_resourceGraph, new DefaultResourceFactory(new ServiceContainer()), new HttpContextAccessor()); + _deserializer = new ResponseDeserializer(_resourceGraph, new DefaultResourceFactory(new ServiceContainer())); _linkValues.Add("self", "http://example.com/articles"); _linkValues.Add("next", "http://example.com/articles?page[offset]=2"); _linkValues.Add("last", "http://example.com/articles?page[offset]=10"); diff --git a/test/UnitTests/Serialization/Common/DocumentParserTests.cs b/test/UnitTests/Serialization/Common/DocumentParserTests.cs index 90947d9e80..843361bf22 100644 --- a/test/UnitTests/Serialization/Common/DocumentParserTests.cs +++ b/test/UnitTests/Serialization/Common/DocumentParserTests.cs @@ -18,7 +18,7 @@ public sealed class BaseDocumentParserTests : DeserializerTestsSetup public BaseDocumentParserTests() { - _deserializer = new TestDocumentParser(_resourceGraph, new DefaultResourceFactory(new ServiceContainer()), new HttpContextAccessor()); + _deserializer = new TestDocumentParser(_resourceGraph, new DefaultResourceFactory(new ServiceContainer())); } [Fact] diff --git a/test/UnitTests/Serialization/DeserializerTestsSetup.cs b/test/UnitTests/Serialization/DeserializerTestsSetup.cs index 6fcd022a25..6d65f31b8e 100644 --- a/test/UnitTests/Serialization/DeserializerTestsSetup.cs +++ b/test/UnitTests/Serialization/DeserializerTestsSetup.cs @@ -11,7 +11,7 @@ public class DeserializerTestsSetup : SerializationTestsSetupBase { protected sealed class TestDocumentParser : BaseDocumentParser { - public TestDocumentParser(IResourceGraph resourceGraph, IResourceFactory resourceFactory, IHttpContextAccessor httpContextAccessor) : base(resourceGraph, resourceFactory, httpContextAccessor) { } + public TestDocumentParser(IResourceGraph resourceGraph, IResourceFactory resourceFactory) : base(resourceGraph, resourceFactory) { } public new object Deserialize(string body) { From 6a682ed8d0350f3da9052502e9e97e369ae5b802 Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Wed, 10 Jun 2020 15:47:31 -0600 Subject: [PATCH 15/26] remove references --- .../Serialization/Client/ResponseDeserializer.cs | 1 - test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs | 1 - test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs | 1 - test/UnitTests/Serialization/Common/DocumentParserTests.cs | 1 - test/UnitTests/Serialization/DeserializerTestsSetup.cs | 1 - 5 files changed, 5 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs index 47cc7a1099..c3210eb131 100644 --- a/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Client/ResponseDeserializer.cs @@ -5,7 +5,6 @@ using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; -using Microsoft.AspNetCore.Http; namespace JsonApiDotNetCore.Serialization.Client { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs index 163b3145a4..9e977dbbc0 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs @@ -14,7 +14,6 @@ using JsonApiDotNetCoreExampleTests.Helpers.Models; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; diff --git a/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs b/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs index 1107bf649d..7c93a01da9 100644 --- a/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs +++ b/test/UnitTests/Serialization/Client/ResponseDeserializerTests.cs @@ -5,7 +5,6 @@ using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Models.Links; using JsonApiDotNetCore.Serialization.Client; -using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Xunit; using UnitTests.TestModels; diff --git a/test/UnitTests/Serialization/Common/DocumentParserTests.cs b/test/UnitTests/Serialization/Common/DocumentParserTests.cs index 843361bf22..c6911cb1f5 100644 --- a/test/UnitTests/Serialization/Common/DocumentParserTests.cs +++ b/test/UnitTests/Serialization/Common/DocumentParserTests.cs @@ -5,7 +5,6 @@ using System.Linq; using JsonApiDotNetCore.Internal; using JsonApiDotNetCore.Models; -using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Xunit; using UnitTests.TestModels; diff --git a/test/UnitTests/Serialization/DeserializerTestsSetup.cs b/test/UnitTests/Serialization/DeserializerTestsSetup.cs index 6d65f31b8e..2d2153b2d7 100644 --- a/test/UnitTests/Serialization/DeserializerTestsSetup.cs +++ b/test/UnitTests/Serialization/DeserializerTestsSetup.cs @@ -3,7 +3,6 @@ using JsonApiDotNetCore.Serialization; using System.Collections.Generic; using JsonApiDotNetCore.Internal; -using Microsoft.AspNetCore.Http; namespace UnitTests.Serialization { From c2f2a69e6f3d2b6f7659527d005b9b43ce67c106 Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Wed, 10 Jun 2020 15:57:27 -0600 Subject: [PATCH 16/26] formatting --- .../Serialization/Common/BaseDocumentParser.cs | 10 ++++------ .../Serialization/Server/RequestDeserializer.cs | 8 +++----- .../Acceptance/Spec/EndToEndTest.cs | 2 -- .../Acceptance/TestFixture.cs | 2 ++ 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs index 37fcb76d34..86b8cf6be8 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -75,15 +75,13 @@ protected virtual IIdentifiable SetAttributes(IIdentifiable entity, Dictionary attributeValues, List attributes) { - foreach (var attr in attributes) + foreach (AttrAttribute attr in attributes) { var disableValidator = false; if (attributeValues == null || attributeValues.Count == 0) @@ -71,7 +71,7 @@ protected override IIdentifiable SetAttributes(IIdentifiable entity, Dictionary< { if (attributeValues.TryGetValue(attr.PublicAttributeName, out object newValue)) { - var convertedValue = ConvertAttrValue(newValue, attr.PropertyInfo.PropertyType); + object convertedValue = ConvertAttrValue(newValue, attr.PropertyInfo.PropertyType); attr.SetValue(entity, convertedValue); AfterProcessField(entity, attr); } @@ -84,10 +84,8 @@ protected override IIdentifiable SetAttributes(IIdentifiable entity, Dictionary< if (!disableValidator) continue; if (_httpContextAccessor?.HttpContext?.Request.Method != HttpMethod.Patch.Method) continue; if (attr.PropertyInfo.GetCustomAttribute() != null) - { _httpContextAccessor?.HttpContext.DisableValidator(attr.PropertyInfo.Name, entity.GetType().Name); - } } return entity; @@ -99,7 +97,7 @@ protected override IIdentifiable SetRelationships(IIdentifiable entity, Dictiona return entity; var entityProperties = entity.GetType().GetProperties(); - foreach (var attr in relationshipAttributes) + foreach (RelationshipAttribute attr in relationshipAttributes) { _httpContextAccessor?.HttpContext?.DisableValidator("Relation", attr.PropertyInfo.Name); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs index 992a39837b..ad99a3da18 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs @@ -17,7 +17,6 @@ using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCoreExampleTests.Helpers.Models; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Logging.Abstractions; using Xunit; @@ -72,7 +71,6 @@ protected IRequestSerializer GetSerializer(Expression(); var options = GetService(); var formatter = new ResourceNameFormatter(options); var resourcesContexts = GetService().GetResourceContexts(); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs index 9e977dbbc0..d6dd9fd675 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TestFixture.cs @@ -31,6 +31,7 @@ public TestFixture() var builder = new WebHostBuilder().UseStartup(); _server = new TestServer(builder); ServiceProvider = _server.Host.Services; + Client = _server.CreateClient(); Context = GetService().GetContext() as AppDbContext; } @@ -59,6 +60,7 @@ public IRequestSerializer GetSerializer(Expression(); + var resourceGraph = new ResourceGraphBuilder(options, NullLoggerFactory.Instance) .AddResource() .AddResource
() From 81e764e5803f00585a2e9d44ca5a0e1b277b48b4 Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Wed, 10 Jun 2020 15:59:08 -0600 Subject: [PATCH 17/26] back to var --- .../Serialization/Common/BaseDocumentParser.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs index 86b8cf6be8..36bd30a382 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -23,7 +23,6 @@ public abstract class BaseDocumentParser { protected readonly IResourceContextProvider _contextProvider; protected readonly IResourceFactory _resourceFactory; - protected Document _document; protected BaseDocumentParser(IResourceContextProvider contextProvider, IResourceFactory resourceFactory) @@ -78,7 +77,7 @@ protected virtual IIdentifiable SetAttributes(IIdentifiable entity, Dictionary Date: Thu, 11 Jun 2020 10:35:49 -0600 Subject: [PATCH 18/26] Requested changes: -mock httpContextAccessor -remove duplicated code -add documentation --- docs/usage/options.md | 19 ++++++++++++ .../Server/RequestDeserializer.cs | 31 +++++-------------- .../Models/ResourceConstructionTests.cs | 16 +++++++--- .../Serialization/DeserializerTestsSetup.cs | 8 +++++ .../Server/RequestDeserializerTests.cs | 2 +- 5 files changed, 47 insertions(+), 29 deletions(-) diff --git a/docs/usage/options.md b/docs/usage/options.md index eefe172e66..3ae2c0066c 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -88,3 +88,22 @@ If you would like to use ASP.NET Core ModelState validation into your controller options.ValidateModelState = true; ``` +# Custom Validators + +Attributes can be marked with custom validators which work with ModelState validation. + +## IsRequired Attribute + +The 'IsRequired' attribute is derived from the 'Required' validator attribute. It accepts a bool to specify if empty strings are allowed on that property. The default for 'AllowEmptyStrings' is false. + +The 'IsRequired' attribute can be used to decorate properties allowing the property to be disabled on PATCH requests, making partial patching possible. + +If a PATCH request contains a property assigned the 'IsRequired' attribute, the requirements of the validator are verified against the patched value. When the validator is enabled, that properties value cannot be null and cannot be empty if 'AllowEmptyStrings' is set to false. + +```c# +public class Person : Identifiable +{ + [IsRequired] + public string FirstName { get; set; } +} +``` diff --git a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs index 3c9114312a..7f03baf6c4 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs @@ -69,47 +69,30 @@ protected override IIdentifiable SetAttributes(IIdentifiable entity, Dictionary< } else { - if (attributeValues.TryGetValue(attr.PublicAttributeName, out object newValue)) - { - object convertedValue = ConvertAttrValue(newValue, attr.PropertyInfo.PropertyType); - attr.SetValue(entity, convertedValue); - AfterProcessField(entity, attr); - } - else + if (!attributeValues.TryGetValue(attr.PublicAttributeName, out object _)) { disableValidator = true; } } if (!disableValidator) continue; - if (_httpContextAccessor?.HttpContext?.Request.Method != HttpMethod.Patch.Method) continue; + if (_httpContextAccessor.HttpContext.Request.Method != HttpMethod.Patch.Method) continue; if (attr.PropertyInfo.GetCustomAttribute() != null) - _httpContextAccessor?.HttpContext.DisableValidator(attr.PropertyInfo.Name, + _httpContextAccessor.HttpContext.DisableValidator(attr.PropertyInfo.Name, entity.GetType().Name); } - return entity; + return base.SetAttributes(entity, attributeValues, attributes); } protected override IIdentifiable SetRelationships(IIdentifiable entity, Dictionary relationshipsValues, List relationshipAttributes) { - if (relationshipsValues == null || relationshipsValues.Count == 0) - return entity; - - var entityProperties = entity.GetType().GetProperties(); foreach (RelationshipAttribute attr in relationshipAttributes) { - _httpContextAccessor?.HttpContext?.DisableValidator("Relation", attr.PropertyInfo.Name); - - if (!relationshipsValues.TryGetValue(attr.PublicRelationshipName, out RelationshipEntry relationshipData) || !relationshipData.IsPopulated) - continue; - - if (attr is HasOneAttribute hasOneAttribute) - SetHasOneRelationship(entity, entityProperties, hasOneAttribute, relationshipData); - else - SetHasManyRelationship(entity, (HasManyAttribute)attr, relationshipData); + _httpContextAccessor.HttpContext.DisableValidator("Relation", attr.PropertyInfo.Name); } - return entity; + + return base.SetRelationships(entity, relationshipsValues, relationshipAttributes); } } } diff --git a/test/UnitTests/Models/ResourceConstructionTests.cs b/test/UnitTests/Models/ResourceConstructionTests.cs index 5c56460e0d..42565f1d68 100644 --- a/test/UnitTests/Models/ResourceConstructionTests.cs +++ b/test/UnitTests/Models/ResourceConstructionTests.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; +using Moq; using Newtonsoft.Json; using Xunit; @@ -17,6 +18,13 @@ 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() { @@ -25,7 +33,7 @@ public void When_resource_has_default_constructor_it_must_succeed() .AddResource() .Build(); - var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(new ServiceContainer()), new TargetedFields(), new HttpContextAccessor()); + var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(new ServiceContainer()), new TargetedFields(), _mockHttpContextAccessor.Object); var body = new { @@ -54,7 +62,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(), new HttpContextAccessor()); + var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(new ServiceContainer()), new TargetedFields(), _mockHttpContextAccessor.Object); var body = new { @@ -90,7 +98,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(), new HttpContextAccessor()); + var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(serviceContainer), new TargetedFields(), _mockHttpContextAccessor.Object); var body = new { @@ -120,7 +128,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(), new HttpContextAccessor()); + 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 2d2153b2d7..669fcf060b 100644 --- a/test/UnitTests/Serialization/DeserializerTestsSetup.cs +++ b/test/UnitTests/Serialization/DeserializerTestsSetup.cs @@ -3,11 +3,19 @@ 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 07edb5e25e..9f31c2dc28 100644 --- a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs +++ b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs @@ -20,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, new HttpContextAccessor()); + _deserializer = new RequestDeserializer(_resourceGraph, new DefaultResourceFactory(new ServiceContainer()), _fieldsManagerMock.Object, _mockHttpContextAccessor.Object); } [Fact] From a1596d584912ea4305d1d177d374395c70db26ca Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Thu, 11 Jun 2020 10:40:02 -0600 Subject: [PATCH 19/26] formatting --- .../Serialization/Common/BaseDocumentParser.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs index 36bd30a382..924dff7c7e 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -72,15 +72,17 @@ protected object Deserialize(string body) protected virtual IIdentifiable SetAttributes(IIdentifiable entity, Dictionary attributeValues, List attributes) { if (attributeValues == null || attributeValues.Count == 0) - return entity; - + return entity; + foreach (AttrAttribute attr in attributes) + { if (attributeValues.TryGetValue(attr.PublicAttributeName, out object newValue)) { var convertedValue = ConvertAttrValue(newValue, attr.PropertyInfo.PropertyType); attr.SetValue(entity, convertedValue); AfterProcessField(entity, attr); } + } return entity; } From d82f1935dd56a7330a67f8e76224e44b8f999c6a Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Thu, 11 Jun 2020 10:42:13 -0600 Subject: [PATCH 20/26] formatting --- .../Serialization/Common/BaseDocumentParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs index 924dff7c7e..74509b6b58 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -74,7 +74,7 @@ protected virtual IIdentifiable SetAttributes(IIdentifiable entity, Dictionary Date: Thu, 11 Jun 2020 10:46:33 -0600 Subject: [PATCH 21/26] change access modifiers --- .../Serialization/Common/BaseDocumentParser.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs index 74509b6b58..7169686f03 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -161,7 +161,7 @@ private IIdentifiable ParseResourceObject(ResourceObject data) /// /// /// - protected void SetHasOneRelationship(IIdentifiable entity, + private void SetHasOneRelationship(IIdentifiable entity, PropertyInfo[] entityProperties, HasOneAttribute attr, RelationshipEntry relationshipData) @@ -224,7 +224,7 @@ private void SetNavigation(IIdentifiable entity, HasOneAttribute attr, string re /// /// Sets a HasMany relationship. /// - protected void SetHasManyRelationship( + private void SetHasManyRelationship( IIdentifiable entity, HasManyAttribute attr, RelationshipEntry relationshipData) @@ -245,7 +245,7 @@ protected void SetHasManyRelationship( AfterProcessField(entity, attr, relationshipData); } - protected object ConvertAttrValue(object newValue, Type targetType) + private object ConvertAttrValue(object newValue, Type targetType) { if (newValue is JContainer jObject) // the attribute value is a complex type that needs additional deserialization From 96cbd84f89f79229c1df0f60ab08a3fd47a41aa9 Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Mon, 15 Jun 2020 12:53:05 -0600 Subject: [PATCH 22/26] Requested changes: -Move integration tests to ModelStateValidationTests -formatting -documentation -comments --- docs/usage/options.md | 13 +- .../Models/Article.cs | 1 - .../JsonApiDotNetCoreExample/Models/Author.cs | 1 - .../ReportsExample/ReportsExample.csproj | 1 - .../IsRequiredAttribute.cs | 6 +- .../Server/RequestDeserializer.cs | 4 +- .../Acceptance/ModelStateValidationTests.cs | 393 ++++++++++++++++++ .../ResourceDefinitionTests.cs | 375 ----------------- .../Models/ResourceConstructionTests.cs | 1 + .../Serialization/DeserializerTestsSetup.cs | 1 + 10 files changed, 402 insertions(+), 394 deletions(-) rename src/JsonApiDotNetCore/Models/{CustomValidators => }/IsRequiredAttribute.cs (88%) diff --git a/docs/usage/options.md b/docs/usage/options.md index 3ae2c0066c..322eff7643 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -87,18 +87,7 @@ If you would like to use ASP.NET Core ModelState validation into your controller ```c# options.ValidateModelState = true; ``` - -# Custom Validators - -Attributes can be marked with custom validators which work with ModelState validation. - -## IsRequired Attribute - -The 'IsRequired' attribute is derived from the 'Required' validator attribute. It accepts a bool to specify if empty strings are allowed on that property. The default for 'AllowEmptyStrings' is false. - -The 'IsRequired' attribute can be used to decorate properties allowing the property to be disabled on PATCH requests, making partial patching possible. - -If a PATCH request contains a property assigned the 'IsRequired' attribute, the requirements of the validator are verified against the patched value. When the validator is enabled, that properties value cannot be null and cannot be empty if 'AllowEmptyStrings' is set to false. +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 diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs index d9d5824b12..bde8b8f310 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using JsonApiDotNetCore.Models; -using JsonApiDotNetCore.Models.CustomValidators; namespace JsonApiDotNetCoreExample.Models { diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs index a3ab102065..7af14d5235 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs @@ -1,6 +1,5 @@ using JsonApiDotNetCore.Models; using System.Collections.Generic; -using JsonApiDotNetCore.Models.CustomValidators; namespace JsonApiDotNetCoreExample.Models { diff --git a/src/Examples/ReportsExample/ReportsExample.csproj b/src/Examples/ReportsExample/ReportsExample.csproj index 534bcd3d7a..e1b25693a5 100644 --- a/src/Examples/ReportsExample/ReportsExample.csproj +++ b/src/Examples/ReportsExample/ReportsExample.csproj @@ -8,7 +8,6 @@ - diff --git a/src/JsonApiDotNetCore/Models/CustomValidators/IsRequiredAttribute.cs b/src/JsonApiDotNetCore/Models/IsRequiredAttribute.cs similarity index 88% rename from src/JsonApiDotNetCore/Models/CustomValidators/IsRequiredAttribute.cs rename to src/JsonApiDotNetCore/Models/IsRequiredAttribute.cs index 8ea2c7f4a4..d28c106e09 100644 --- a/src/JsonApiDotNetCore/Models/CustomValidators/IsRequiredAttribute.cs +++ b/src/JsonApiDotNetCore/Models/IsRequiredAttribute.cs @@ -1,11 +1,11 @@ +using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -using System.ComponentModel.DataAnnotations; using JsonApiDotNetCore.Extensions; -namespace JsonApiDotNetCore.Models.CustomValidators +namespace JsonApiDotNetCore.Models { - public class IsRequiredAttribute : RequiredAttribute + public sealed class IsRequiredAttribute : RequiredAttribute { private bool _isDisabled; diff --git a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs index 7f03baf6c4..5ab8633287 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Reflection; using JsonApiDotNetCore.Extensions; -using JsonApiDotNetCore.Models.CustomValidators; using System.Net.Http; namespace JsonApiDotNetCore.Serialization.Server @@ -87,6 +86,9 @@ protected override IIdentifiable SetAttributes(IIdentifiable entity, Dictionary< 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); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs index 7907b28305..c8af84c8b8 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs @@ -1,13 +1,17 @@ +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; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCoreExampleTests.Acceptance.Spec; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using Xunit; @@ -15,9 +19,25 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance { public sealed class ModelStateValidationTests : FunctionalTestCollection { + private readonly AppDbContext _context; + 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; + + _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] @@ -167,5 +187,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(new 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(new 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(new 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(new 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(new 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(new 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(new 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(new 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 f66a1b56bf..8629e4f4b9 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs @@ -613,380 +613,5 @@ public async Task Cascade_Permission_Error_Delete_ToMany_Relationship() Assert.Equal("You are not allowed to update fields or relationships of locked todo items.", errorDocument.Errors[0].Title); Assert.Null(errorDocument.Errors[0].Detail); } - - [Fact] - public async Task Create_Article_With_IsRequired_Name_Attribute_Succeeds() - { - // Arrange - string name = "Article Title"; - var context = _fixture.GetService(); - var author = _authorFaker.Generate(); - context.AuthorDifferentDbContextName.Add(author); - await context.SaveChangesAsync(); - - var route = "/api/v1/articles"; - var request = new HttpRequestMessage(new 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 _fixture.Client.SendAsync(request); - - // Assert - var body = await response.Content.ReadAsStringAsync(); - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - - var articleResponse = _fixture.GetDeserializer().DeserializeSingle
(body).Data; - Assert.NotNull(articleResponse); - - var persistedArticle = await _fixture.Context.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 = _fixture.GetService(); - var author = _authorFaker.Generate(); - context.AuthorDifferentDbContextName.Add(author); - await context.SaveChangesAsync(); - - var route = "/api/v1/articles"; - var request = new HttpRequestMessage(new 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 _fixture.Client.SendAsync(request); - - // Assert - var body = await response.Content.ReadAsStringAsync(); - Assert.Equal(HttpStatusCode.Created, response.StatusCode); - - var articleResponse = _fixture.GetDeserializer().DeserializeSingle
(body).Data; - Assert.NotNull(articleResponse); - - var persistedArticle = await _fixture.Context.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 = _fixture.GetService(); - var author = _authorFaker.Generate(); - context.AuthorDifferentDbContextName.Add(author); - await context.SaveChangesAsync(); - - var route = "/api/v1/articles"; - var request = new HttpRequestMessage(new 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 _fixture.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 = _fixture.GetService(); - var author = _authorFaker.Generate(); - context.AuthorDifferentDbContextName.Add(author); - await context.SaveChangesAsync(); - - var route = "/api/v1/articles"; - var request = new HttpRequestMessage(new 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 _fixture.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 = _fixture.GetService(); - var article = _articleFaker.Generate(); - context.Articles.Add(article); - await context.SaveChangesAsync(); - - var route = $"/api/v1/articles/{article.Id}"; - var request = new HttpRequestMessage(new 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 _fixture.Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - _fixture.ReloadDbContext(); - var persistedArticle = await _fixture.Context.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 = _fixture.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(new 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 _fixture.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 = _fixture.GetService(); - var article = _articleFaker.Generate(); - context.Articles.Add(article); - await context.SaveChangesAsync(); - - var route = $"/api/v1/articles/{article.Id}"; - var request = new HttpRequestMessage(new 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 _fixture.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 = _fixture.GetService(); - var article = _articleFaker.Generate(); - context.Articles.Add(article); - await context.SaveChangesAsync(); - - var route = $"/api/v1/articles/{article.Id}"; - var request = new HttpRequestMessage(new 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 _fixture.Client.SendAsync(request); - - // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - _fixture.ReloadDbContext(); - var persistedArticle = await _fixture.Context.Articles - .SingleOrDefaultAsync(a => a.Id == article.Id); - - var updatedName = persistedArticle.Name; - Assert.Equal("", updatedName); - } } } diff --git a/test/UnitTests/Models/ResourceConstructionTests.cs b/test/UnitTests/Models/ResourceConstructionTests.cs index 42565f1d68..044fe6300f 100644 --- a/test/UnitTests/Models/ResourceConstructionTests.cs +++ b/test/UnitTests/Models/ResourceConstructionTests.cs @@ -19,6 +19,7 @@ namespace UnitTests.Models public sealed class ResourceConstructionTests { public Mock _mockHttpContextAccessor; + public ResourceConstructionTests() { _mockHttpContextAccessor = new Mock(); diff --git a/test/UnitTests/Serialization/DeserializerTestsSetup.cs b/test/UnitTests/Serialization/DeserializerTestsSetup.cs index 669fcf060b..3d35316cf4 100644 --- a/test/UnitTests/Serialization/DeserializerTestsSetup.cs +++ b/test/UnitTests/Serialization/DeserializerTestsSetup.cs @@ -11,6 +11,7 @@ namespace UnitTests.Serialization public class DeserializerTestsSetup : SerializationTestsSetupBase { public Mock _mockHttpContextAccessor; + public DeserializerTestsSetup() { _mockHttpContextAccessor = new Mock(); From 945691d0cc2f9eef37d9e913b67fb3ff937dca50 Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Mon, 15 Jun 2020 14:38:50 -0600 Subject: [PATCH 23/26] refactor --- .../Server/RequestDeserializer.cs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs index 5ab8633287..9c37c960ac 100644 --- a/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs +++ b/src/JsonApiDotNetCore/Serialization/Server/RequestDeserializer.cs @@ -59,26 +59,21 @@ protected override void AfterProcessField(IIdentifiable entity, IResourceField f protected override IIdentifiable SetAttributes(IIdentifiable entity, Dictionary attributeValues, List attributes) { - foreach (AttrAttribute attr in attributes) + if (_httpContextAccessor.HttpContext.Request.Method == HttpMethod.Patch.Method) { - var disableValidator = false; - if (attributeValues == null || attributeValues.Count == 0) + foreach (AttrAttribute attr in attributes) { - disableValidator = true; - } - else - { - if (!attributeValues.TryGetValue(attr.PublicAttributeName, out object _)) + if (attr.PropertyInfo.GetCustomAttribute() != null) { - disableValidator = true; + bool disableValidator = attributeValues == null || attributeValues.Count == 0 || + !attributeValues.TryGetValue(attr.PublicAttributeName, out _); + + if (disableValidator) + { + _httpContextAccessor.HttpContext.DisableValidator(attr.PropertyInfo.Name, entity.GetType().Name); + } } } - - if (!disableValidator) continue; - if (_httpContextAccessor.HttpContext.Request.Method != HttpMethod.Patch.Method) continue; - if (attr.PropertyInfo.GetCustomAttribute() != null) - _httpContextAccessor.HttpContext.DisableValidator(attr.PropertyInfo.Name, - entity.GetType().Name); } return base.SetAttributes(entity, attributeValues, attributes); From 4496d724db789753c57166888860912b2935de96 Mon Sep 17 00:00:00 2001 From: Sarah McQueary Date: Mon, 15 Jun 2020 14:59:13 -0600 Subject: [PATCH 24/26] remoev comma. --- docs/usage/options.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/options.md b/docs/usage/options.md index 322eff7643..dc13b46544 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -87,7 +87,7 @@ 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. +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 From 7eeb6c5d7b7ce1460233fd98d6cd5765b4b9ca68 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 16 Jun 2020 11:00:09 +0200 Subject: [PATCH 25/26] Cleanup --- docs/usage/options.md | 4 +-- .../Acceptance/ModelStateValidationTests.cs | 30 ++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/docs/usage/options.md b/docs/usage/options.md index 8ffaed3b5d..4f52a93883 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -91,9 +91,9 @@ 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 +public class Person : Identifiable { - [IsRequired] + [IsRequired(AllowEmptyStrings = true)] public string FirstName { get; set; } } ``` diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs index c8af84c8b8..093ddf4a8c 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs @@ -7,7 +7,6 @@ using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Models.JsonApiDocuments; -using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCoreExampleTests.Acceptance.Spec; @@ -19,25 +18,28 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance { public sealed class ModelStateValidationTests : FunctionalTestCollection { - private readonly AppDbContext _context; private readonly Faker
_articleFaker; private readonly Faker _authorFaker; private readonly Faker _tagFaker; + public ModelStateValidationTests(StandardApplicationFactory factory) : base(factory) { - var options = (JsonApiOptions)_factory.GetService(); + var options = (JsonApiOptions) _factory.GetService(); options.ValidateModelState = true; - _context = _factory.GetService(); + 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)); + .CustomInstantiator(f => new Tag(context)) + .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10)); } [Fact] @@ -199,7 +201,7 @@ public async Task Create_Article_With_IsRequired_Name_Attribute_Succeeds() await context.SaveChangesAsync(); var route = "/api/v1/articles"; - var request = new HttpRequestMessage(new HttpMethod("POST"), route); + var request = new HttpRequestMessage(HttpMethod.Post, route); var content = new { data = new @@ -253,7 +255,7 @@ public async Task Create_Article_With_IsRequired_Name_Attribute_Empty_Succeeds() await context.SaveChangesAsync(); var route = "/api/v1/articles"; - var request = new HttpRequestMessage(new HttpMethod("POST"), route); + var request = new HttpRequestMessage(HttpMethod.Post, route); var content = new { data = new @@ -306,7 +308,7 @@ public async Task Create_Article_With_IsRequired_Name_Attribute_Explicitly_Null_ await context.SaveChangesAsync(); var route = "/api/v1/articles"; - var request = new HttpRequestMessage(new HttpMethod("POST"), route); + var request = new HttpRequestMessage(HttpMethod.Post, route); var content = new { data = new @@ -356,7 +358,7 @@ public async Task Create_Article_With_IsRequired_Name_Attribute_Missing_Fails() await context.SaveChangesAsync(); var route = "/api/v1/articles"; - var request = new HttpRequestMessage(new HttpMethod("POST"), route); + var request = new HttpRequestMessage(HttpMethod.Post, route); var content = new { data = new @@ -403,7 +405,7 @@ public async Task Update_Article_With_IsRequired_Name_Attribute_Succeeds() await context.SaveChangesAsync(); var route = $"/api/v1/articles/{article.Id}"; - var request = new HttpRequestMessage(new HttpMethod("PATCH"), route); + var request = new HttpRequestMessage(HttpMethod.Patch, route); var content = new { data = new @@ -445,7 +447,7 @@ public async Task Update_Article_With_IsRequired_Name_Attribute_Missing_Succeeds await context.SaveChangesAsync(); var route = $"/api/v1/articles/{article.Id}"; - var request = new HttpRequestMessage(new HttpMethod("PATCH"), route); + var request = new HttpRequestMessage(HttpMethod.Patch, route); var content = new { data = new @@ -490,7 +492,7 @@ public async Task Update_Article_With_IsRequired_Name_Attribute_Explicitly_Null_ await context.SaveChangesAsync(); var route = $"/api/v1/articles/{article.Id}"; - var request = new HttpRequestMessage(new HttpMethod("PATCH"), route); + var request = new HttpRequestMessage(HttpMethod.Patch, route); var content = new { data = new @@ -531,7 +533,7 @@ public async Task Update_Article_With_IsRequired_Name_Attribute_Empty_Succeeds() await context.SaveChangesAsync(); var route = $"/api/v1/articles/{article.Id}"; - var request = new HttpRequestMessage(new HttpMethod("PATCH"), route); + var request = new HttpRequestMessage(HttpMethod.Patch, route); var content = new { data = new From 44a9d92501cc6dae21b6d9b7a52da3ed1d5ba3c9 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Tue, 16 Jun 2020 11:02:51 +0200 Subject: [PATCH 26/26] removed unneeded code --- .../Acceptance/ModelStateValidationTests.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs index 093ddf4a8c..20ecf96a5a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs @@ -60,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); @@ -134,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);