diff --git a/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs b/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs
index 0bf055cfdb..b47e87d636 100644
--- a/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs
+++ b/src/JsonApiDotNetCore/Middleware/CurrentRequestMiddleware.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel;
using System.Linq;
using System.Threading.Tasks;
using JsonApiDotNetCore.Configuration;
@@ -6,6 +7,7 @@
using JsonApiDotNetCore.Internal.Contracts;
using JsonApiDotNetCore.Managers.Contracts;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Primitives;
@@ -21,6 +23,7 @@ public class CurrentRequestMiddleware
private ICurrentRequest _currentRequest;
private IResourceGraph _resourceGraph;
private IJsonApiOptions _options;
+ private RouteValueDictionary _routeValues;
private IControllerResourceMapping _controllerResourceMapping;
public CurrentRequestMiddleware(RequestDelegate next)
@@ -39,12 +42,15 @@ public async Task Invoke(HttpContext httpContext,
_controllerResourceMapping = controllerResourceMapping;
_resourceGraph = resourceGraph;
_options = options;
+ _routeValues = httpContext.GetRouteData().Values;
var requestResource = GetCurrentEntity();
if (requestResource != null)
{
- _currentRequest.SetRequestResource(GetCurrentEntity());
+ _currentRequest.SetRequestResource(requestResource);
_currentRequest.IsRelationshipPath = PathIsRelationship();
- _currentRequest.BasePath = GetBasePath(_currentRequest.GetRequestResource().ResourceName);
+ _currentRequest.BasePath = GetBasePath(requestResource.ResourceName);
+ _currentRequest.BaseId = GetBaseId();
+ _currentRequest.RelationshipId = GetRelationshipId();
}
if (IsValid())
@@ -53,50 +59,127 @@ public async Task Invoke(HttpContext httpContext,
}
}
- private string GetBasePath(string entityName)
+ private string GetBaseId()
{
- var r = _httpContext.Request;
- if (_options.RelativeLinks)
+ var resource = _currentRequest.GetRequestResource();
+ var individualComponents = SplitCurrentPath();
+ if (individualComponents.Length < 2)
{
- return GetNamespaceFromPath(r.Path, entityName);
+ return null;
}
- return $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}";
+ var indexOfResource = individualComponents.ToList().FindIndex(c => c == resource.ResourceName);
+ var baseId = individualComponents.ElementAtOrDefault(indexOfResource + 1);
+ if (baseId == null)
+ {
+ return null;
+ }
+ CheckIdType(baseId, resource.IdentityType);
+ return baseId;
}
+ private string GetRelationshipId()
+ {
+ var resource = _currentRequest.GetRequestResource();
+ if (!_currentRequest.IsRelationshipPath)
+ {
+ return null;
+ }
+ var components = SplitCurrentPath();
+ var toReturn = components.ElementAtOrDefault(4);
- internal static string GetNamespaceFromPath(string path, string entityName)
+ if (toReturn == null)
+ {
+ return null;
+ }
+ var relType = _currentRequest.RequestRelationship.RightType;
+ var relResource = _resourceGraph.GetResourceContext(relType);
+ var relIdentityType = relResource.IdentityType;
+ CheckIdType(toReturn, relIdentityType);
+ return toReturn;
+ }
+ private string[] SplitCurrentPath()
{
- var entityNameSpan = entityName.AsSpan();
- var pathSpan = path.AsSpan();
- const char delimiter = '/';
- for (var i = 0; i < pathSpan.Length; i++)
+ var path = _httpContext.Request.Path.Value;
+ var ns = $"/{GetNameSpace()}";
+ var nonNameSpaced = path.Replace(ns, "");
+ nonNameSpaced = nonNameSpaced.Trim('/');
+ var individualComponents = nonNameSpaced.Split('/');
+ return individualComponents;
+ }
+
+
+ private void CheckIdType(string value, Type idType)
+ {
+ try
{
- if (pathSpan[i].Equals(delimiter))
+ var converter = TypeDescriptor.GetConverter(idType);
+ if (converter != null)
{
- var nextPosition = i + 1;
- if (pathSpan.Length > i + entityNameSpan.Length)
+ if (!converter.IsValid(value))
{
- var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length);
- if (entityNameSpan.SequenceEqual(possiblePathSegment))
+ throw new JsonApiException(500, $"We could not convert the id '{value}'");
+ }
+ else
+ {
+ if (idType == typeof(int))
{
- // check to see if it's the last position in the string
- // or if the next character is a /
- var lastCharacterPosition = nextPosition + entityNameSpan.Length;
-
- if (lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition].Equals(delimiter))
+ if ((int)converter.ConvertFromString(value) < 0)
{
- return pathSpan.Slice(0, i).ToString();
+ throw new JsonApiException(500, "The base ID is an integer, and it is negative.");
}
}
}
}
}
+ catch (NotSupportedException)
+ {
+
+ }
- return string.Empty;
+ }
+
+ private string GetBasePath(string resourceName = null)
+ {
+ var r = _httpContext.Request;
+ if (_options.RelativeLinks)
+ {
+ return GetNameSpace(resourceName);
+ }
+ var ns = GetNameSpace(resourceName);
+ var customRoute = GetCustomRoute(r.Path.Value, resourceName);
+ var toReturn = $"{r.Scheme}://{r.Host}/{ns}";
+ if(customRoute != null)
+ {
+ toReturn += $"/{customRoute}";
+ }
+ return toReturn;
+ }
+
+ private object GetCustomRoute(string path, string resourceName)
+ {
+ var ns = GetNameSpace();
+ var trimmedComponents = path.Trim('/').Split('/').ToList();
+ var resourceNameIndex = trimmedComponents.FindIndex(c => c == resourceName);
+ var newComponents = trimmedComponents.Take(resourceNameIndex ).ToArray();
+ var customRoute = string.Join('/', newComponents);
+ if(customRoute == ns)
+ {
+ return null;
+ }
+ else
+ {
+ return customRoute;
+ }
+ }
+
+ private string GetNameSpace(string resourceName = null)
+ {
+
+ return _options.Namespace;
}
protected bool PathIsRelationship()
{
- var actionName = (string)_httpContext.GetRouteData().Values["action"];
+ var actionName = (string)_routeValues["action"];
return actionName.ToLower().Contains("relationships");
}
@@ -124,7 +207,9 @@ private bool IsValidAcceptHeader(HttpContext context)
foreach (var acceptHeader in acceptHeaders)
{
if (ContainsMediaTypeParameters(acceptHeader) == false)
+ {
continue;
+ }
FlushResponse(context, 406);
return false;
@@ -165,16 +250,21 @@ private void FlushResponse(HttpContext context, int statusCode)
///
private ResourceContext GetCurrentEntity()
{
- var controllerName = (string)_httpContext.GetRouteValue("controller");
+ var controllerName = (string)_routeValues["controller"];
if (controllerName == null)
+ {
return null;
+ }
var resourceType = _controllerResourceMapping.GetAssociatedResource(controllerName);
var requestResource = _resourceGraph.GetResourceContext(resourceType);
if (requestResource == null)
+ {
return requestResource;
- var rd = _httpContext.GetRouteData().Values;
- if (rd.TryGetValue("relationshipName", out object relationshipName))
+ }
+ if (_routeValues.TryGetValue("relationshipName", out object relationshipName))
+ {
_currentRequest.RequestRelationship = requestResource.Relationships.Single(r => r.PublicRelationshipName == (string)relationshipName);
+ }
return requestResource;
}
}
diff --git a/src/JsonApiDotNetCore/RequestServices/Contracts/ICurrentRequest.cs b/src/JsonApiDotNetCore/RequestServices/Contracts/ICurrentRequest.cs
index 0260db8c91..15eddf495e 100644
--- a/src/JsonApiDotNetCore/RequestServices/Contracts/ICurrentRequest.cs
+++ b/src/JsonApiDotNetCore/RequestServices/Contracts/ICurrentRequest.cs
@@ -29,6 +29,8 @@ public interface ICurrentRequest
/// is the relationship attribute associated with the targeted relationship
///
RelationshipAttribute RequestRelationship { get; set; }
+ string BaseId { get; set; }
+ string RelationshipId { get; set; }
///
/// Sets the current context entity for this entire request
diff --git a/src/JsonApiDotNetCore/RequestServices/CurrentRequest.cs b/src/JsonApiDotNetCore/RequestServices/CurrentRequest.cs
index abb1b5863a..053cf39cfb 100644
--- a/src/JsonApiDotNetCore/RequestServices/CurrentRequest.cs
+++ b/src/JsonApiDotNetCore/RequestServices/CurrentRequest.cs
@@ -10,6 +10,8 @@ class CurrentRequest : ICurrentRequest
public string BasePath { get; set; }
public bool IsRelationshipPath { get; set; }
public RelationshipAttribute RequestRelationship { get; set; }
+ public string BaseId { get; set; }
+ public string RelationshipId { get; set; }
///
/// The main resource of the request.
diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs
index 4797732c6b..a03a213534 100644
--- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs
+++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomControllerTests.cs
@@ -128,7 +128,7 @@ public async Task CustomRouteControllers_Creates_Proper_Relationship_Links()
var deserializedBody = JsonConvert.DeserializeObject(body);
var result = deserializedBody["data"]["relationships"]["owner"]["links"]["related"].ToString();
- Assert.EndsWith($"{route}/owner", deserializedBody["data"]["relationships"]["owner"]["links"]["related"].ToString());
+ Assert.EndsWith($"{route}/owner", result);
}
}
}
diff --git a/test/UnitTests/Middleware/CurrentRequestMiddlewareTests.cs b/test/UnitTests/Middleware/CurrentRequestMiddlewareTests.cs
new file mode 100644
index 0000000000..01b26a69b1
--- /dev/null
+++ b/test/UnitTests/Middleware/CurrentRequestMiddlewareTests.cs
@@ -0,0 +1,209 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Internal;
+using JsonApiDotNetCore.Internal.Contracts;
+using JsonApiDotNetCore.Managers;
+using JsonApiDotNetCore.Middleware;
+using JsonApiDotNetCore.Models;
+using JsonApiDotNetCoreExample.Models;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.AspNetCore.Routing;
+using Moq;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace UnitTests.Middleware
+{
+ public class CurrentRequestMiddlewareTests
+ {
+ [Fact]
+ public async Task ParseUrlBase_UrlHasBaseIdSet_ShouldSetCurrentRequestWithSaidId()
+ {
+ // Arrange
+ var id = "123";
+ var configuration = GetConfiguration($"/users/{id}");
+ var currentRequest = configuration.CurrentRequest;
+
+ // Act
+ await RunMiddlewareTask(configuration);
+
+ // Assert
+ Assert.Equal(id, currentRequest.BaseId);
+ }
+
+ [Fact]
+ public async Task ParseUrlBase_UrlHasNoBaseIdSet_ShouldHaveBaseIdSetToNull()
+ {
+ // Arrange
+ var configuration = GetConfiguration("/users");
+ var currentRequest = configuration.CurrentRequest;
+
+ // Act
+ await RunMiddlewareTask(configuration);
+
+ // Assert
+ Assert.Null(currentRequest.BaseId);
+ }
+ [Fact]
+ public async Task ParseUrlRel_UrlHasRelationshipIdSet_ShouldHaveBaseIdAndRelationshipIdSet()
+ {
+ // Arrange
+ var baseId = "5";
+ var relId = "23";
+ var configuration = GetConfiguration($"/users/{baseId}/relationships/books/{relId}", relType: typeof(TodoItem), relIdType: typeof(int));
+ var currentRequest = configuration.CurrentRequest;
+
+ // Act
+ await RunMiddlewareTask(configuration);
+
+ // Assert
+ Assert.Equal(baseId, currentRequest.BaseId);
+ Assert.Equal(relId, currentRequest.RelationshipId);
+ }
+
+ [Fact]
+ public async Task ParseUrlBase_UrlHasNegativeBaseIdAndTypeIsInt_ShouldThrowJAException()
+ {
+ // Arrange
+ var configuration = GetConfiguration("/users/-5/");
+
+ // Act
+ var task = RunMiddlewareTask(configuration);
+
+ // Assert
+ var exception = await Assert.ThrowsAsync(async () =>
+ {
+ await task;
+ });
+ Assert.Equal(500, exception.GetStatusCode());
+ Assert.Contains("negative", exception.Message);
+ }
+
+ [Theory]
+ [InlineData("12315K", typeof(int), true)]
+ [InlineData("12315K", typeof(int), false)]
+ [InlineData("5", typeof(Guid), true)]
+ [InlineData("5", typeof(Guid), false)]
+ public async Task ParseUrlBase_UrlHasIncorrectBaseIdSet_ShouldThrowException(string baseId, Type idType, bool addSlash)
+ {
+ // Arrange
+ var url = addSlash ? $"/users/{baseId}/" : $"/users/{baseId}";
+ var configuration = GetConfiguration(url, idType: idType);
+
+ // Act
+ var task = RunMiddlewareTask(configuration);
+
+ // Assert
+ var exception = await Assert.ThrowsAsync(async () =>
+ {
+ await task;
+ });
+ Assert.Equal(500, exception.GetStatusCode());
+ Assert.Contains(baseId, exception.Message);
+ }
+
+ class InvokeConfiguration
+ {
+ public CurrentRequestMiddleware MiddleWare;
+ public HttpContext HttpContext;
+ public Mock ControllerResourcemapping;
+ public Mock Options;
+ public CurrentRequest CurrentRequest;
+ public Mock ResourceGraph;
+ }
+ private Task RunMiddlewareTask(InvokeConfiguration holder)
+ {
+ var controllerResourceMapping = holder.ControllerResourcemapping.Object;
+ var context = holder.HttpContext;
+ var options = holder.Options.Object;
+ var currentRequest = holder.CurrentRequest;
+ var resourceGraph = holder.ResourceGraph.Object;
+ return holder.MiddleWare.Invoke(context, controllerResourceMapping, options, currentRequest, resourceGraph);
+ }
+ private InvokeConfiguration GetConfiguration(string path, string resourceName = "users", Type idType = null, Type relType = null, Type relIdType = null)
+ {
+ if((relType != null) != (relIdType != null))
+ {
+ throw new ArgumentException("Define both reltype and relidType or dont.");
+ }
+ if (path.First() != '/')
+ {
+ throw new ArgumentException("Path should start with a '/'");
+ }
+ idType ??= typeof(int);
+ var middleware = new CurrentRequestMiddleware((context) =>
+ {
+ return Task.Run(() => Console.WriteLine("finished"));
+ });
+ var forcedNamespace = "api/v1";
+ var mockMapping = new Mock();
+ Mock mockOptions = CreateMockOptions(forcedNamespace);
+ var mockGraph = CreateMockResourceGraph(idType, resourceName, relIdType : relIdType);
+ var currentRequest = new CurrentRequest();
+ if (relType != null && relIdType != null)
+ {
+ currentRequest.RequestRelationship = new HasManyAttribute
+ {
+ RightType = relType
+ };
+ }
+ var context = CreateHttpContext(path, isRelationship: relType != null);
+ return new InvokeConfiguration
+ {
+ MiddleWare = middleware,
+ ControllerResourcemapping = mockMapping,
+ Options = mockOptions,
+ CurrentRequest = currentRequest,
+ HttpContext = context,
+ ResourceGraph = mockGraph
+ };
+ }
+
+ private static Mock CreateMockOptions(string forcedNamespace)
+ {
+ var mockOptions = new Mock();
+ mockOptions.Setup(o => o.Namespace).Returns(forcedNamespace);
+ return mockOptions;
+ }
+
+ private static DefaultHttpContext CreateHttpContext(string path, bool isRelationship = false)
+ {
+ var context = new DefaultHttpContext();
+ context.Request.Path = new PathString(path);
+ context.Response.Body = new MemoryStream();
+ var feature = new RouteValuesFeature();
+ feature.RouteValues["controller"] = "fake!";
+ feature.RouteValues["action"] = isRelationship ? "relationships" : "noRel";
+ context.Features.Set(feature);
+ return context;
+ }
+
+ private Mock CreateMockResourceGraph(Type idType, string resourceName, Type relIdType = null)
+ {
+ var mockGraph = new Mock();
+ var resourceContext = new ResourceContext
+ {
+ ResourceName = resourceName,
+ IdentityType = idType
+ };
+ var seq = mockGraph.SetupSequence(d => d.GetResourceContext(It.IsAny())).Returns(resourceContext);
+ if (relIdType != null)
+ {
+ var relResourceContext = new ResourceContext
+ {
+ ResourceName = "todoItems",
+ IdentityType = relIdType
+ };
+ seq.Returns(relResourceContext);
+ }
+ return mockGraph;
+ }
+
+ }
+}