Skip to content

Commit 390db09

Browse files
authored
Merge pull request #316 from sigent/feature/#252_valid_modelstate
Feature/#252 valid modelstate
2 parents 4481147 + 4d0d422 commit 390db09

File tree

5 files changed

+147
-1
lines changed

5 files changed

+147
-1
lines changed

Diff for: src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

+10
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ public class JsonApiOptions
125125
/// </remarks>
126126
public bool EnableOperations { get; set; }
127127

128+
/// <summary>
129+
/// Whether or not to validate model state.
130+
/// </summary>
131+
/// <example>
132+
/// <code>
133+
/// options.ValidateModelState = true;
134+
/// </code>
135+
/// </example>
136+
public bool ValidateModelState { get; set; }
137+
128138
[Obsolete("JsonContract resolver can now be set on SerializerSettings.")]
129139
public IContractResolver JsonContractResolver
130140
{

Diff for: src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Generic;
22
using System.Threading.Tasks;
3+
using JsonApiDotNetCore.Extensions;
34
using JsonApiDotNetCore.Internal;
45
using JsonApiDotNetCore.Models;
56
using JsonApiDotNetCore.Services;
@@ -152,6 +153,8 @@ public virtual async Task<IActionResult> PostAsync([FromBody] T entity)
152153

153154
if (!_jsonApiContext.Options.AllowClientGeneratedIds && !string.IsNullOrEmpty(entity.StringId))
154155
return Forbidden();
156+
if (_jsonApiContext.Options.ValidateModelState && !ModelState.IsValid)
157+
return BadRequest(ModelState.ConvertToErrorCollection());
155158

156159
entity = await _create.CreateAsync(entity);
157160

@@ -164,6 +167,8 @@ public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] T entity)
164167

165168
if (entity == null)
166169
return UnprocessableEntity();
170+
if (_jsonApiContext.Options.ValidateModelState && !ModelState.IsValid)
171+
return BadRequest(ModelState.ConvertToErrorCollection());
167172

168173
var updatedEntity = await _update.UpdateAsync(id, entity);
169174

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using JsonApiDotNetCore.Internal;
2+
using Microsoft.AspNetCore.Mvc.ModelBinding;
3+
using Microsoft.EntityFrameworkCore.Internal;
4+
5+
namespace JsonApiDotNetCore.Extensions
6+
{
7+
public static class ModelStateExtensions
8+
{
9+
public static ErrorCollection ConvertToErrorCollection(this ModelStateDictionary modelState)
10+
{
11+
ErrorCollection errors = new ErrorCollection();
12+
foreach (var entry in modelState)
13+
{
14+
if (!entry.Value.Errors.Any())
15+
continue;
16+
foreach (var modelError in entry.Value.Errors)
17+
{
18+
errors.Errors.Add(new Error(400, entry.Key, modelError.ErrorMessage, modelError.Exception != null ? ErrorMeta.FromException(modelError.Exception) : null));
19+
}
20+
}
21+
return errors;
22+
}
23+
}
24+
}

Diff for: src/JsonApiDotNetCore/JsonApiDotNetCore.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<VersionPrefix>2.3.2</VersionPrefix>
3+
<VersionPrefix>2.3.3</VersionPrefix>
44
<TargetFrameworks>$(NetStandardVersion)</TargetFrameworks>
55
<AssemblyName>JsonApiDotNetCore</AssemblyName>
66
<PackageId>JsonApiDotNetCore</PackageId>

Diff for: test/UnitTests/Controllers/BaseJsonApiController_Tests.cs

+107
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
using Moq;
66
using Xunit;
77
using System.Threading.Tasks;
8+
using JsonApiDotNetCore.Configuration;
89
using JsonApiDotNetCore.Internal;
10+
using Microsoft.AspNetCore.Http;
11+
using Microsoft.AspNetCore.Mvc;
912

1013
namespace UnitTests
1114
{
@@ -143,6 +146,8 @@ public async Task PatchAsync_Calls_Service()
143146
const int id = 0;
144147
var resource = new Resource();
145148
var serviceMock = new Mock<IUpdateService<Resource>>();
149+
_jsonApiContextMock.Setup(a => a.ApplyContext<Resource>(It.IsAny<BaseJsonApiController<Resource>>())).Returns(_jsonApiContextMock.Object);
150+
_jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions());
146151
var controller = new BaseJsonApiController<Resource>(_jsonApiContextMock.Object, update: serviceMock.Object);
147152

148153
// act
@@ -153,6 +158,47 @@ public async Task PatchAsync_Calls_Service()
153158
VerifyApplyContext();
154159
}
155160

161+
[Fact]
162+
public async Task PatchAsync_ModelStateInvalid_ValidateModelStateDisbled()
163+
{
164+
// arrange
165+
const int id = 0;
166+
var resource = new Resource();
167+
var serviceMock = new Mock<IUpdateService<Resource>>();
168+
_jsonApiContextMock.Setup(a => a.ApplyContext<Resource>(It.IsAny<BaseJsonApiController<Resource>>())).Returns(_jsonApiContextMock.Object);
169+
_jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = false });
170+
var controller = new BaseJsonApiController<Resource>(_jsonApiContextMock.Object, update: serviceMock.Object);
171+
172+
// act
173+
var response = await controller.PatchAsync(id, resource);
174+
175+
// assert
176+
serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny<Resource>()), Times.Once);
177+
VerifyApplyContext();
178+
Assert.IsNotType<BadRequestObjectResult>(response);
179+
}
180+
181+
[Fact]
182+
public async Task PatchAsync_ModelStateInvalid_ValidateModelStateEnabled()
183+
{
184+
// arrange
185+
const int id = 0;
186+
var resource = new Resource();
187+
var serviceMock = new Mock<IUpdateService<Resource>>();
188+
_jsonApiContextMock.Setup(a => a.ApplyContext<Resource>(It.IsAny<BaseJsonApiController<Resource>>())).Returns(_jsonApiContextMock.Object);
189+
_jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions{ValidateModelState = true});
190+
var controller = new BaseJsonApiController<Resource>(_jsonApiContextMock.Object, update: serviceMock.Object);
191+
controller.ModelState.AddModelError("Id", "Failed Validation");
192+
193+
// act
194+
var response = await controller.PatchAsync(id, resource);
195+
196+
// assert
197+
serviceMock.Verify(m => m.UpdateAsync(id, It.IsAny<Resource>()), Times.Never);
198+
Assert.IsType<BadRequestObjectResult>(response);
199+
Assert.IsType<ErrorCollection>(((BadRequestObjectResult) response).Value);
200+
}
201+
156202
[Fact]
157203
public async Task PatchAsync_Throws_405_If_No_Service()
158204
{
@@ -168,6 +214,67 @@ public async Task PatchAsync_Throws_405_If_No_Service()
168214
Assert.Equal(405, exception.GetStatusCode());
169215
}
170216

217+
[Fact]
218+
public async Task PostAsync_Calls_Service()
219+
{
220+
// arrange
221+
var resource = new Resource();
222+
var serviceMock = new Mock<ICreateService<Resource>>();
223+
_jsonApiContextMock.Setup(a => a.ApplyContext<Resource>(It.IsAny<BaseJsonApiController<Resource>>())).Returns(_jsonApiContextMock.Object);
224+
_jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions());
225+
var controller = new BaseJsonApiController<Resource>(_jsonApiContextMock.Object, create: serviceMock.Object);
226+
serviceMock.Setup(m => m.CreateAsync(It.IsAny<Resource>())).ReturnsAsync(resource);
227+
controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext {HttpContext = new DefaultHttpContext()};
228+
229+
// act
230+
await controller.PostAsync(resource);
231+
232+
// assert
233+
serviceMock.Verify(m => m.CreateAsync(It.IsAny<Resource>()), Times.Once);
234+
VerifyApplyContext();
235+
}
236+
237+
[Fact]
238+
public async Task PostAsync_ModelStateInvalid_ValidateModelStateDisabled()
239+
{
240+
// arrange
241+
var resource = new Resource();
242+
var serviceMock = new Mock<ICreateService<Resource>>();
243+
_jsonApiContextMock.Setup(a => a.ApplyContext<Resource>(It.IsAny<BaseJsonApiController<Resource>>())).Returns(_jsonApiContextMock.Object);
244+
_jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = false });
245+
var controller = new BaseJsonApiController<Resource>(_jsonApiContextMock.Object, create: serviceMock.Object);
246+
serviceMock.Setup(m => m.CreateAsync(It.IsAny<Resource>())).ReturnsAsync(resource);
247+
controller.ControllerContext = new Microsoft.AspNetCore.Mvc.ControllerContext { HttpContext = new DefaultHttpContext() };
248+
249+
// act
250+
var response = await controller.PostAsync(resource);
251+
252+
// assert
253+
serviceMock.Verify(m => m.CreateAsync(It.IsAny<Resource>()), Times.Once);
254+
VerifyApplyContext();
255+
Assert.IsNotType<BadRequestObjectResult>(response);
256+
}
257+
258+
[Fact]
259+
public async Task PostAsync_ModelStateInvalid_ValidateModelStateEnabled()
260+
{
261+
// arrange
262+
var resource = new Resource();
263+
var serviceMock = new Mock<ICreateService<Resource>>();
264+
_jsonApiContextMock.Setup(a => a.ApplyContext<Resource>(It.IsAny<BaseJsonApiController<Resource>>())).Returns(_jsonApiContextMock.Object);
265+
_jsonApiContextMock.SetupGet(a => a.Options).Returns(new JsonApiOptions { ValidateModelState = true });
266+
var controller = new BaseJsonApiController<Resource>(_jsonApiContextMock.Object, create: serviceMock.Object);
267+
controller.ModelState.AddModelError("Id", "Failed Validation");
268+
269+
// act
270+
var response = await controller.PostAsync(resource);
271+
272+
// assert
273+
serviceMock.Verify(m => m.CreateAsync(It.IsAny<Resource>()), Times.Never);
274+
Assert.IsType<BadRequestObjectResult>(response);
275+
Assert.IsType<ErrorCollection>(((BadRequestObjectResult)response).Value);
276+
}
277+
171278
[Fact]
172279
public async Task PatchRelationshipsAsync_Calls_Service()
173280
{

0 commit comments

Comments
 (0)