Skip to content

Commit 6b16ea3

Browse files
author
Bart Koelman
authored
At startup, a single instance of CurrentRequestMiddleware is created, which is used to process multiple requests in parallel. Therefore it cannot contain request-specific state in its fields, because parallel requests then overwrite the state of each other. (#728)
As a result of overwriting state, the FilterService is unable to find the matching CurrentRequest data, causing a NullReferenceException in its constructor.
1 parent 74f32f7 commit 6b16ea3

File tree

2 files changed

+83
-62
lines changed

2 files changed

+83
-62
lines changed

src/Examples/GettingStarted/Startup.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ public void ConfigureServices(IServiceCollection services)
2222

2323
public void Configure(IApplicationBuilder app, SampleDbContext context)
2424
{
25-
context.Database.EnsureDeleted(); // indices need to be reset
25+
// indices need to be reset
26+
context.Database.EnsureDeleted();
27+
context.Database.EnsureCreated();
2628
app.UseJsonApi();
2729
}
2830
}

src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs

+80-61
Original file line numberDiff line numberDiff line change
@@ -22,100 +22,94 @@ namespace JsonApiDotNetCore.Middleware
2222
public sealed class CurrentRequestMiddleware
2323
{
2424
private readonly RequestDelegate _next;
25-
private HttpContext _httpContext;
26-
private ICurrentRequest _currentRequest;
27-
private IResourceGraph _resourceGraph;
28-
private IJsonApiOptions _options;
29-
private RouteValueDictionary _routeValues;
30-
private IControllerResourceMapping _controllerResourceMapping;
3125

3226
public CurrentRequestMiddleware(RequestDelegate next)
3327
{
3428
_next = next;
3529
}
3630

3731
public async Task Invoke(HttpContext httpContext,
38-
IControllerResourceMapping controllerResourceMapping,
39-
IJsonApiOptions options,
40-
ICurrentRequest currentRequest,
41-
IResourceGraph resourceGraph)
32+
IControllerResourceMapping controllerResourceMapping,
33+
IJsonApiOptions options,
34+
ICurrentRequest currentRequest,
35+
IResourceGraph resourceGraph)
4236
{
43-
_httpContext = httpContext;
44-
_currentRequest = currentRequest;
45-
_controllerResourceMapping = controllerResourceMapping;
46-
_resourceGraph = resourceGraph;
47-
_options = options;
48-
_routeValues = httpContext.GetRouteData().Values;
49-
var requestResource = GetCurrentEntity();
37+
var routeValues = httpContext.GetRouteData().Values;
38+
var requestContext = new RequestContext(httpContext, currentRequest, resourceGraph, options, routeValues,
39+
controllerResourceMapping);
40+
41+
var requestResource = GetCurrentEntity(requestContext);
5042
if (requestResource != null)
5143
{
52-
_currentRequest.SetRequestResource(requestResource);
53-
_currentRequest.IsRelationshipPath = PathIsRelationship();
54-
_currentRequest.BasePath = GetBasePath(requestResource.ResourceName);
55-
_currentRequest.BaseId = GetBaseId();
56-
_currentRequest.RelationshipId = GetRelationshipId();
44+
requestContext.CurrentRequest.SetRequestResource(requestResource);
45+
requestContext.CurrentRequest.IsRelationshipPath = PathIsRelationship(requestContext.RouteValues);
46+
requestContext.CurrentRequest.BasePath = GetBasePath(requestContext, requestResource.ResourceName);
47+
requestContext.CurrentRequest.BaseId = GetBaseId(requestContext.RouteValues);
48+
requestContext.CurrentRequest.RelationshipId = GetRelationshipId(requestContext);
5749
}
5850

59-
if (await IsValidAsync())
51+
if (await IsValidAsync(requestContext))
6052
{
61-
await _next(httpContext);
53+
await _next(requestContext.HttpContext);
6254
}
6355
}
6456

65-
private string GetBaseId()
57+
private static string GetBaseId(RouteValueDictionary routeValues)
6658
{
67-
if (_routeValues.TryGetValue("id", out object stringId))
59+
if (routeValues.TryGetValue("id", out object stringId))
6860
{
6961
return (string)stringId;
7062
}
7163

7264
return null;
7365
}
74-
private string GetRelationshipId()
66+
67+
private static string GetRelationshipId(RequestContext requestContext)
7568
{
76-
if (!_currentRequest.IsRelationshipPath)
69+
if (!requestContext.CurrentRequest.IsRelationshipPath)
7770
{
7871
return null;
7972
}
80-
var components = SplitCurrentPath();
73+
var components = SplitCurrentPath(requestContext);
8174
var toReturn = components.ElementAtOrDefault(4);
8275

8376
return toReturn;
8477
}
85-
private string[] SplitCurrentPath()
78+
79+
private static string[] SplitCurrentPath(RequestContext requestContext)
8680
{
87-
var path = _httpContext.Request.Path.Value;
88-
var ns = $"/{_options.Namespace}";
81+
var path = requestContext.HttpContext.Request.Path.Value;
82+
var ns = $"/{requestContext.Options.Namespace}";
8983
var nonNameSpaced = path.Replace(ns, "");
9084
nonNameSpaced = nonNameSpaced.Trim('/');
9185
var individualComponents = nonNameSpaced.Split('/');
9286
return individualComponents;
9387
}
9488

95-
private string GetBasePath(string resourceName = null)
89+
private static string GetBasePath(RequestContext requestContext, string resourceName = null)
9690
{
97-
var r = _httpContext.Request;
98-
if (_options.RelativeLinks)
91+
var r = requestContext.HttpContext.Request;
92+
if (requestContext.Options.RelativeLinks)
9993
{
100-
return _options.Namespace;
94+
return requestContext.Options.Namespace;
10195
}
10296

103-
var customRoute = GetCustomRoute(r.Path.Value, resourceName);
104-
var toReturn = $"{r.Scheme}://{r.Host}/{_options.Namespace}";
97+
var customRoute = GetCustomRoute(requestContext.Options, r.Path.Value, resourceName);
98+
var toReturn = $"{r.Scheme}://{r.Host}/{requestContext.Options.Namespace}";
10599
if (customRoute != null)
106100
{
107101
toReturn += $"/{customRoute}";
108102
}
109103
return toReturn;
110104
}
111105

112-
private object GetCustomRoute(string path, string resourceName)
106+
private static object GetCustomRoute(IJsonApiOptions options, string path, string resourceName)
113107
{
114108
var trimmedComponents = path.Trim('/').Split('/').ToList();
115109
var resourceNameIndex = trimmedComponents.FindIndex(c => c == resourceName);
116110
var newComponents = trimmedComponents.Take(resourceNameIndex).ToArray();
117111
var customRoute = string.Join('/', newComponents);
118-
if (customRoute == _options.Namespace)
112+
if (customRoute == options.Namespace)
119113
{
120114
return null;
121115
}
@@ -125,23 +119,23 @@ private object GetCustomRoute(string path, string resourceName)
125119
}
126120
}
127121

128-
private bool PathIsRelationship()
122+
private static bool PathIsRelationship(RouteValueDictionary routeValues)
129123
{
130-
var actionName = (string)_routeValues["action"];
124+
var actionName = (string)routeValues["action"];
131125
return actionName.ToLowerInvariant().Contains("relationships");
132126
}
133127

134-
private async Task<bool> IsValidAsync()
128+
private static async Task<bool> IsValidAsync(RequestContext requestContext)
135129
{
136-
return await IsValidContentTypeHeaderAsync(_httpContext) && await IsValidAcceptHeaderAsync(_httpContext);
130+
return await IsValidContentTypeHeaderAsync(requestContext) && await IsValidAcceptHeaderAsync(requestContext);
137131
}
138132

139-
private async Task<bool> IsValidContentTypeHeaderAsync(HttpContext context)
133+
private static async Task<bool> IsValidContentTypeHeaderAsync(RequestContext requestContext)
140134
{
141-
var contentType = context.Request.ContentType;
135+
var contentType = requestContext.HttpContext.Request.ContentType;
142136
if (contentType != null && ContainsMediaTypeParameters(contentType))
143137
{
144-
await FlushResponseAsync(context, new Error(HttpStatusCode.UnsupportedMediaType)
138+
await FlushResponseAsync(requestContext, new Error(HttpStatusCode.UnsupportedMediaType)
145139
{
146140
Title = "The specified Content-Type header value is not supported.",
147141
Detail = $"Please specify '{HeaderConstants.ContentType}' for the Content-Type header value."
@@ -152,9 +146,9 @@ private async Task<bool> IsValidContentTypeHeaderAsync(HttpContext context)
152146
return true;
153147
}
154148

155-
private async Task<bool> IsValidAcceptHeaderAsync(HttpContext context)
149+
private static async Task<bool> IsValidAcceptHeaderAsync(RequestContext requestContext)
156150
{
157-
if (context.Request.Headers.TryGetValue(HeaderConstants.AcceptHeader, out StringValues acceptHeaders) == false)
151+
if (requestContext.HttpContext.Request.Headers.TryGetValue(HeaderConstants.AcceptHeader, out StringValues acceptHeaders) == false)
158152
return true;
159153

160154
foreach (var acceptHeader in acceptHeaders)
@@ -164,7 +158,7 @@ private async Task<bool> IsValidAcceptHeaderAsync(HttpContext context)
164158
continue;
165159
}
166160

167-
await FlushResponseAsync(context, new Error(HttpStatusCode.NotAcceptable)
161+
await FlushResponseAsync(requestContext, new Error(HttpStatusCode.NotAcceptable)
168162
{
169163
Title = "The specified Accept header value is not supported.",
170164
Detail = $"Please specify '{HeaderConstants.ContentType}' for the Accept header value."
@@ -195,11 +189,11 @@ private static bool ContainsMediaTypeParameters(string mediaType)
195189
);
196190
}
197191

198-
private async Task FlushResponseAsync(HttpContext context, Error error)
192+
private static async Task FlushResponseAsync(RequestContext requestContext, Error error)
199193
{
200-
context.Response.StatusCode = (int) error.StatusCode;
194+
requestContext.HttpContext.Response.StatusCode = (int) error.StatusCode;
201195

202-
JsonSerializer serializer = JsonSerializer.CreateDefault(_options.SerializerSettings);
196+
JsonSerializer serializer = JsonSerializer.CreateDefault(requestContext.Options.SerializerSettings);
203197
serializer.ApplyErrorSettings();
204198

205199
// https://github.com/JamesNK/Newtonsoft.Json/issues/1193
@@ -212,34 +206,59 @@ private async Task FlushResponseAsync(HttpContext context, Error error)
212206
}
213207

214208
stream.Seek(0, SeekOrigin.Begin);
215-
await stream.CopyToAsync(context.Response.Body);
209+
await stream.CopyToAsync(requestContext.HttpContext.Response.Body);
216210
}
217211

218-
context.Response.Body.Flush();
212+
requestContext.HttpContext.Response.Body.Flush();
219213
}
220214

221215
/// <summary>
222216
/// Gets the current entity that we need for serialization and deserialization.
223217
/// </summary>
224218
/// <returns></returns>
225-
private ResourceContext GetCurrentEntity()
219+
private static ResourceContext GetCurrentEntity(RequestContext requestContext)
226220
{
227-
var controllerName = (string)_routeValues["controller"];
221+
var controllerName = (string)requestContext.RouteValues["controller"];
228222
if (controllerName == null)
229223
{
230224
return null;
231225
}
232-
var resourceType = _controllerResourceMapping.GetAssociatedResource(controllerName);
233-
var requestResource = _resourceGraph.GetResourceContext(resourceType);
226+
var resourceType = requestContext.ControllerResourceMapping.GetAssociatedResource(controllerName);
227+
var requestResource = requestContext.ResourceGraph.GetResourceContext(resourceType);
234228
if (requestResource == null)
235229
{
236230
return null;
237231
}
238-
if (_routeValues.TryGetValue("relationshipName", out object relationshipName))
232+
if (requestContext.RouteValues.TryGetValue("relationshipName", out object relationshipName))
239233
{
240-
_currentRequest.RequestRelationship = requestResource.Relationships.SingleOrDefault(r => r.PublicRelationshipName == (string)relationshipName);
234+
requestContext.CurrentRequest.RequestRelationship = requestResource.Relationships.SingleOrDefault(r => r.PublicRelationshipName == (string)relationshipName);
241235
}
242236
return requestResource;
243237
}
238+
239+
private sealed class RequestContext
240+
{
241+
public HttpContext HttpContext { get; }
242+
public ICurrentRequest CurrentRequest { get; }
243+
public IResourceGraph ResourceGraph { get; }
244+
public IJsonApiOptions Options { get; }
245+
public RouteValueDictionary RouteValues { get; }
246+
public IControllerResourceMapping ControllerResourceMapping { get; }
247+
248+
public RequestContext(HttpContext httpContext,
249+
ICurrentRequest currentRequest,
250+
IResourceGraph resourceGraph,
251+
IJsonApiOptions options,
252+
RouteValueDictionary routeValues,
253+
IControllerResourceMapping controllerResourceMapping)
254+
{
255+
HttpContext = httpContext;
256+
CurrentRequest = currentRequest;
257+
ResourceGraph = resourceGraph;
258+
Options = options;
259+
RouteValues = routeValues;
260+
ControllerResourceMapping = controllerResourceMapping;
261+
}
262+
}
244263
}
245264
}

0 commit comments

Comments
 (0)