Skip to content

Commit a801d95

Browse files
maureiwisepotato
authored andcommitted
Fix/#620 (#621)
* chore: improve testability by exposing SingleData and ManyData on Document * fix: #620 * chore: fix problem in which custom outputter is not executed * chore: enable detailed errors * docs: add comment
1 parent c4f6327 commit a801d95

File tree

8 files changed

+87
-46
lines changed

8 files changed

+87
-46
lines changed

src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public virtual void Configure(
5858
AppDbContext context)
5959
{
6060
context.Database.EnsureCreated();
61+
app.EnableDetailedErrors();
6162
app.UseJsonApi();
6263
}
6364

src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ public class JsonApiOptions : IJsonApiOptions
3232
/// <summary>
3333
/// Whether or not stack traces should be serialized in Error objects
3434
/// </summary>
35-
public static bool DisableErrorStackTraces { get; set; }
35+
public static bool DisableErrorStackTraces { get; set; } = true;
3636

3737
/// <summary>
3838
/// Whether or not source URLs should be serialized in Error objects
3939
/// </summary>
40-
public static bool DisableErrorSource { get; set; }
40+
public static bool DisableErrorSource { get; set; } = true;
4141

4242
/// <summary>
4343
/// Whether or not ResourceHooks are enabled.

src/JsonApiDotNetCore/Controllers/BaseJsonApiController.cs

+19-14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System;
2-
using System.Reflection;
31
using System.Threading.Tasks;
42
using JsonApiDotNetCore.Configuration;
53
using JsonApiDotNetCore.Extensions;
@@ -106,7 +104,11 @@ public virtual async Task<IActionResult> GetAsync(TId id)
106104
if (_getById == null) throw Exceptions.UnSupportedRequestMethod;
107105
var entity = await _getById.GetAsync(id);
108106
if (entity == null)
109-
return NotFound();
107+
{
108+
// remove the null argument as soon as this has been resolved:
109+
// https://github.com/aspnet/AspNetCore/issues/16969
110+
return NotFound(null);
111+
}
110112

111113
return Ok(entity);
112114
}
@@ -117,7 +119,11 @@ public virtual async Task<IActionResult> GetRelationshipsAsync(TId id, string re
117119
throw Exceptions.UnSupportedRequestMethod;
118120
var relationship = await _getRelationships.GetRelationshipsAsync(id, relationshipName);
119121
if (relationship == null)
120-
return NotFound();
122+
{
123+
// remove the null argument as soon as this has been resolved:
124+
// https://github.com/aspnet/AspNetCore/issues/16969
125+
return NotFound(null);
126+
}
121127

122128
return Ok(relationship);
123129
}
@@ -141,7 +147,7 @@ public virtual async Task<IActionResult> PostAsync([FromBody] T entity)
141147
return Forbidden();
142148

143149
if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid)
144-
return UnprocessableEntity(ModelState.ConvertToErrorCollection<T>(GetAssociatedResource()));
150+
return UnprocessableEntity(ModelState.ConvertToErrorCollection<T>());
145151

146152
entity = await _create.CreateAsync(entity);
147153

@@ -155,12 +161,17 @@ public virtual async Task<IActionResult> PatchAsync(TId id, [FromBody] T entity)
155161
return UnprocessableEntity();
156162

157163
if (_jsonApiOptions.ValidateModelState && !ModelState.IsValid)
158-
return UnprocessableEntity(ModelState.ConvertToErrorCollection<T>(GetAssociatedResource()));
164+
return UnprocessableEntity(ModelState.ConvertToErrorCollection<T>());
159165

160166
var updatedEntity = await _update.UpdateAsync(id, entity);
161167

162168
if (updatedEntity == null)
163-
return NotFound();
169+
{
170+
// remove the null argument as soon as this has been resolved:
171+
// https://github.com/aspnet/AspNetCore/issues/16969
172+
return NotFound(null);
173+
}
174+
164175

165176
return Ok(updatedEntity);
166177
}
@@ -180,14 +191,8 @@ public virtual async Task<IActionResult> DeleteAsync(TId id)
180191
return NotFound();
181192
return NoContent();
182193
}
183-
184-
internal Type GetAssociatedResource()
185-
{
186-
return GetType().GetMethod(nameof(GetAssociatedResource), BindingFlags.Instance | BindingFlags.NonPublic)
187-
.DeclaringType
188-
.GetGenericArguments()[0];
189-
}
190194
}
195+
191196
public class BaseJsonApiController<T>
192197
: BaseJsonApiController<T, int>
193198
where T : class, IIdentifiable<int>

src/JsonApiDotNetCore/Extensions/IApplicationBuilderExtensions.cs

+8-8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Microsoft.AspNetCore.Builder;
77
using Microsoft.AspNetCore.Hosting;
88
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.Extensions.Hosting;
910
using Microsoft.Extensions.Logging;
1011

1112
namespace JsonApiDotNetCore.Extensions
@@ -20,7 +21,6 @@ public static class IApplicationBuilderExtensions
2021
/// <returns></returns>
2122
public static void UseJsonApi(this IApplicationBuilder app)
2223
{
23-
DisableDetailedErrorsIfProduction(app);
2424
LogResourceGraphValidations(app);
2525
using (var scope = app.ApplicationServices.CreateScope())
2626
{
@@ -38,14 +38,14 @@ public static void UseJsonApi(this IApplicationBuilder app)
3838
app.UseEndpoints(endpoints => endpoints.MapControllers());
3939
}
4040

41-
private static void DisableDetailedErrorsIfProduction(IApplicationBuilder app)
41+
/// <summary>
42+
/// Configures your application to return stack traces in error results.
43+
/// </summary>
44+
/// <param name="app"></param>
45+
public static void EnableDetailedErrors(this IApplicationBuilder app)
4246
{
43-
var webHostEnvironment = (IWebHostEnvironment) app.ApplicationServices.GetService(typeof(IWebHostEnvironment));
44-
if (webHostEnvironment.EnvironmentName == "Production")
45-
{
46-
JsonApiOptions.DisableErrorStackTraces = true;
47-
JsonApiOptions.DisableErrorSource = true;
48-
}
47+
JsonApiOptions.DisableErrorStackTraces = false;
48+
JsonApiOptions.DisableErrorSource = false;
4949
}
5050

5151
private static void LogResourceGraphValidations(IApplicationBuilder app)

src/JsonApiDotNetCore/Extensions/ModelStateExtensions.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace JsonApiDotNetCore.Extensions
99
{
1010
public static class ModelStateExtensions
1111
{
12-
public static ErrorCollection ConvertToErrorCollection<T>(this ModelStateDictionary modelState, Type resourceType)
12+
public static ErrorCollection ConvertToErrorCollection<T>(this ModelStateDictionary modelState) where T : class, IIdentifiable
1313
{
1414
ErrorCollection collection = new ErrorCollection();
1515
foreach (var entry in modelState)
@@ -19,7 +19,7 @@ public static ErrorCollection ConvertToErrorCollection<T>(this ModelStateDiction
1919
continue;
2020
}
2121

22-
var targetedProperty = resourceType.GetProperty(entry.Key);
22+
var targetedProperty = typeof(T).GetProperty(entry.Key);
2323
var attrName = targetedProperty.GetCustomAttribute<AttrAttribute>().PublicAttributeName;
2424

2525
foreach (var modelError in entry.Value.Errors)

src/JsonApiDotNetCore/Formatters/JsonApiWriter.cs

+25-18
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Threading.Tasks;
44
using JsonApiDotNetCore.Internal;
55
using JsonApiDotNetCore.Serialization.Server;
6+
using Microsoft.AspNetCore.Mvc;
67
using Microsoft.AspNetCore.Mvc.Formatters;
78
using Microsoft.Extensions.Logging;
89
using Newtonsoft.Json;
@@ -32,33 +33,39 @@ public async Task WriteAsync(OutputFormatterWriteContext context)
3233
throw new ArgumentNullException(nameof(context));
3334

3435
var response = context.HttpContext.Response;
35-
using (var writer = context.WriterFactory(response.Body, Encoding.UTF8))
36+
using var writer = context.WriterFactory(response.Body, Encoding.UTF8);
37+
string responseContent;
38+
39+
if (_serializer == null)
40+
{
41+
responseContent = JsonConvert.SerializeObject(context.Object);
42+
}
43+
else
3644
{
3745
response.ContentType = Constants.ContentType;
38-
string responseContent;
39-
if (_serializer == null)
46+
try
4047
{
41-
responseContent = JsonConvert.SerializeObject(context.Object);
42-
}
43-
else
44-
{
45-
try
48+
if (context.Object is ProblemDetails pd)
4649
{
47-
responseContent = _serializer.Serialize(context.Object);
48-
}
49-
catch (Exception e)
50-
{
51-
_logger?.LogError(new EventId(), e, "An error ocurred while formatting the response");
5250
var errors = new ErrorCollection();
53-
errors.Add(new Error(400, e.Message, ErrorMeta.FromException(e)));
51+
errors.Add(new Error(pd.Status.Value, pd.Title, pd.Detail));
5452
responseContent = _serializer.Serialize(errors);
55-
response.StatusCode = 400;
53+
} else
54+
{
55+
responseContent = _serializer.Serialize(context.Object);
5656
}
5757
}
58-
59-
await writer.WriteAsync(responseContent);
60-
await writer.FlushAsync();
58+
catch (Exception e)
59+
{
60+
_logger?.LogError(new EventId(), e, "An error ocurred while formatting the response");
61+
var errors = new ErrorCollection();
62+
errors.Add(new Error(500, e.Message, ErrorMeta.FromException(e)));
63+
responseContent = _serializer.Serialize(errors);
64+
response.StatusCode = 500;
65+
}
6166
}
67+
await writer.WriteAsync(responseContent);
68+
await writer.FlushAsync();
6269
}
6370
}
6471
}

src/JsonApiDotNetCore/Models/JsonApiDocuments/ExposableData.cs

+4-2
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ public bool ShouldSerializeData()
3131
/// <summary>
3232
/// Internally used for "single" primary data.
3333
/// </summary>
34-
internal T SingleData { get; private set; }
34+
[JsonIgnore]
35+
public T SingleData { get; private set; }
3536

3637
/// <summary>
3738
/// Internally used for "many" primary data.
3839
/// </summary>
39-
internal List<T> ManyData { get; private set; }
40+
[JsonIgnore]
41+
public List<T> ManyData { get; private set; }
4042

4143
/// <summary>
4244
/// Internally used to indicate if the document's primary data is

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/FetchingDataTests.cs

+26
Original file line numberDiff line numberDiff line change
@@ -123,5 +123,31 @@ public async Task GetResources_NoDefaultPageSize_ReturnsResources()
123123
// Assert
124124
Assert.True(result.Data.Count >= 20);
125125
}
126+
127+
[Fact]
128+
public async Task GetSingleResource_ResourceDoesNotExist_ReturnsNotFoundWithNullData()
129+
{
130+
// Arrange
131+
var context = _fixture.GetService<AppDbContext>();
132+
context.TodoItems.RemoveRange(context.TodoItems);
133+
await context.SaveChangesAsync();
134+
135+
var builder = new WebHostBuilder()
136+
.UseStartup<NoDefaultPageSizeStartup>();
137+
var httpMethod = new HttpMethod("GET");
138+
var route = $"/api/v1/todoItems/123";
139+
var server = new TestServer(builder);
140+
var client = server.CreateClient();
141+
var request = new HttpRequestMessage(httpMethod, route);
142+
143+
// Act
144+
var response = await client.SendAsync(request);
145+
var body = await response.Content.ReadAsStringAsync();
146+
var document = JsonConvert.DeserializeObject<Document>(body);
147+
148+
// Assert
149+
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
150+
Assert.Null(document.Data);
151+
}
126152
}
127153
}

0 commit comments

Comments
 (0)