diff --git a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs
index 502d678f0d..54fcf5afaf 100644
--- a/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs
+++ b/src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs
@@ -125,6 +125,16 @@ public class JsonApiOptions
///
public bool EnableOperations { get; set; }
+ ///
+ /// Whether or not to validate model state.
+ ///
+ ///
+ ///
+ /// options.ValidateModelState = true;
+ ///
+ ///
+ public bool ValidateModelState { get; set; }
+
[Obsolete("JsonContract resolver can now be set on SerializerSettings.")]
public IContractResolver JsonContractResolver
{
diff --git a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs
index 760f8f8d56..f4041e97d6 100644
--- a/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs
+++ b/src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
+using JsonApiDotNetCore.Extensions;
using JsonApiDotNetCore.Internal;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Services;
@@ -152,6 +153,8 @@ public virtual async Task PostAsync([FromBody] T entity)
if (!_jsonApiContext.Options.AllowClientGeneratedIds && !string.IsNullOrEmpty(entity.StringId))
return Forbidden();
+ if (_jsonApiContext.Options.ValidateModelState && !ModelState.IsValid)
+ return BadRequest(ModelState.ConvertToErrorCollection());
entity = await _create.CreateAsync(entity);
@@ -164,6 +167,8 @@ public virtual async Task PatchAsync(TId id, [FromBody] T entity)
if (entity == null)
return UnprocessableEntity();
+ if (_jsonApiContext.Options.ValidateModelState && !ModelState.IsValid)
+ return BadRequest(ModelState.ConvertToErrorCollection());
var updatedEntity = await _update.UpdateAsync(id, entity);
diff --git a/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs b/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs
new file mode 100644
index 0000000000..8eb0fc95f7
--- /dev/null
+++ b/src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs
@@ -0,0 +1,24 @@
+using JsonApiDotNetCore.Internal;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using Microsoft.EntityFrameworkCore.Internal;
+
+namespace JsonApiDotNetCore.Extensions
+{
+ public static class ModelStateExtensions
+ {
+ public static ErrorCollection ConvertToErrorCollection(this ModelStateDictionary modelState)
+ {
+ ErrorCollection errors = new ErrorCollection();
+ foreach (var entry in modelState)
+ {
+ if (!entry.Value.Errors.Any())
+ continue;
+ foreach (var modelError in entry.Value.Errors)
+ {
+ errors.Errors.Add(new Error(400, entry.Key, modelError.ErrorMessage, modelError.Exception != null ? ErrorMeta.FromException(modelError.Exception) : null));
+ }
+ }
+ return errors;
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
index 75fc955402..cc337c28dc 100755
--- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
+++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
@@ -1,6 +1,6 @@
- 2.3.2
+ 2.3.3
$(NetStandardVersion)
JsonApiDotNetCore
JsonApiDotNetCore
diff --git a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs b/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs
index 9c59372846..d2d3ac8319 100644
--- a/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs
+++ b/test/UnitTests/Controllers/BaseJsonApiController_Tests.cs
@@ -5,7 +5,10 @@
using Moq;
using Xunit;
using System.Threading.Tasks;
+using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Internal;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
namespace UnitTests
{
@@ -143,6 +146,8 @@ public async Task PatchAsync_Calls_Service()
const int id = 0;
var resource = new Resource();
var serviceMock = new Mock>();
+ _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object);
+ _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions());
var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object);
// act
@@ -153,6 +158,47 @@ public async Task PatchAsync_Calls_Service()
VerifyApplyContext();
}
+ [Fact]
+ public async Task PatchAsync_ModelStateInvalid_ValidateModelStateDisbled()
+ {
+ // arrange
+ const int id = 0;
+ var resource = new Resource();
+ var serviceMock = new Mock>();
+ _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object);
+ _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = false });
+ var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object);
+
+ // act
+ var response = await controller.PatchAsync(id, resource);
+
+ // assert
+ serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Once);
+ VerifyApplyContext();
+ Assert.IsNotType(response);
+ }
+
+ [Fact]
+ public async Task PatchAsync_ModelStateInvalid_ValidateModelStateEnabled()
+ {
+ // arrange
+ const int id = 0;
+ var resource = new Resource();
+ var serviceMock = new Mock>();
+ _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object);
+ _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions{ValidateModelState = true});
+ var controller = new BaseJsonApiController(_jsonApiContextMock.Object, update: serviceMock.Object);
+ controller.ModelState.AddModelError("Id", "Failed Validation");
+
+ // act
+ var response = await controller.PatchAsync(id, resource);
+
+ // assert
+ serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny()), Times.Never);
+ Assert.IsType(response);
+ Assert.IsType(((BadRequestObjectResult) response).Value);
+ }
+
[Fact]
public async Task PatchAsync_Throws_405_If_No_Service()
{
@@ -168,6 +214,67 @@ public async Task PatchAsync_Throws_405_If_No_Service()
Assert.Equal(405, exception.GetStatusCode());
}
+ [Fact]
+ public async Task PostAsync_Calls_Service()
+ {
+ // arrange
+ var resource = new Resource();
+ var serviceMock = new Mock>();
+ _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object);
+ _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions());
+ var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object);
+ serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource);
+ controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext {HttpContext = new DefaultHttpContext()};
+
+ // act
+ await controller.PostAsync(resource);
+
+ // assert
+ serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Once);
+ VerifyApplyContext();
+ }
+
+ [Fact]
+ public async Task PostAsync_ModelStateInvalid_ValidateModelStateDisabled()
+ {
+ // arrange
+ var resource = new Resource();
+ var serviceMock = new Mock>();
+ _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object);
+ _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = false });
+ var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object);
+ serviceMock.Setup(m => m.CreateAsync(It.IsAny())).ReturnsAsync(resource);
+ controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext { HttpContext = new DefaultHttpContext() };
+
+ // act
+ var response = await controller.PostAsync(resource);
+
+ // assert
+ serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Once);
+ VerifyApplyContext();
+ Assert.IsNotType(response);
+ }
+
+ [Fact]
+ public async Task PostAsync_ModelStateInvalid_ValidateModelStateEnabled()
+ {
+ // arrange
+ var resource = new Resource();
+ var serviceMock = new Mock>();
+ _jsonApiContextMock.Setup(a => a.ApplyContext(It.IsAny>())).Returns(_jsonApiContextMock.Object);
+ _jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = true });
+ var controller = new BaseJsonApiController(_jsonApiContextMock.Object, create: serviceMock.Object);
+ controller.ModelState.AddModelError("Id", "Failed Validation");
+
+ // act
+ var response = await controller.PostAsync(resource);
+
+ // assert
+ serviceMock.Verify(m => m.CreateAsync(It.IsAny()), Times.Never);
+ Assert.IsType(response);
+ Assert.IsType(((BadRequestObjectResult)response).Value);
+ }
+
[Fact]
public async Task PatchRelationshipsAsync_Calls_Service()
{