diff --git a/.travis.yml b/.travis.yml
index b8a1d74643..7ac38bba9f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,7 +6,7 @@ services:
before_script:
- psql -c 'create database JsonApiDotNetCoreExample;' -U postgres
mono: none
-dotnet: 2.0.3 # https://www.microsoft.com/net/download/linux
+dotnet: 2.1.105 # https://www.microsoft.com/net/download/linux
branches:
only:
- master
diff --git a/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.JsonApiContext.PathIsRelationship_Benchmarks-report-github.md b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.JsonApiContext.PathIsRelationship_Benchmarks-report-github.md
new file mode 100644
index 0000000000..6be58e241a
--- /dev/null
+++ b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.JsonApiContext.PathIsRelationship_Benchmarks-report-github.md
@@ -0,0 +1,12 @@
+```ini
+BenchmarkDotNet=v0.10.10, OS=Mac OS X 10.12
+Processor=Intel Core i5-5257U CPU 2.70GHz (Broadwell), ProcessorCount=4
+.NET Core SDK=2.1.4
+ [Host] : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT
+ DefaultJob : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT
+```
+
+| Method | Mean | Error | StdDev | Gen 0 | Allocated |
+| ---------- | --------: | ---------: | ---------: | -----: | --------: |
+| UsingSplit | 421.08 ns | 19.3905 ns | 54.0529 ns | 0.4725 | 744 B |
+| Current | 52.23 ns | 0.8052 ns | 0.7532 ns | - | 0 B |
diff --git a/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.LinkBuilder.LinkBuilder_GetNamespaceFromPath_Benchmarks-report-github.md b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.LinkBuilder.LinkBuilder_GetNamespaceFromPath_Benchmarks-report-github.md
new file mode 100644
index 0000000000..72951396e8
--- /dev/null
+++ b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.LinkBuilder.LinkBuilder_GetNamespaceFromPath_Benchmarks-report-github.md
@@ -0,0 +1,16 @@
+``` ini
+
+BenchmarkDotNet=v0.10.10, OS=Mac OS X 10.12
+Processor=Intel Core i5-5257U CPU 2.70GHz (Broadwell), ProcessorCount=4
+.NET Core SDK=2.1.4
+ [Host] : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT
+ Job-XFMVNE : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT
+
+LaunchCount=3 TargetCount=20 WarmupCount=10
+
+```
+| Method | Mean | Error | StdDev | Gen 0 | Allocated |
+|--------------------------- |-----------:|----------:|----------:|-------:|----------:|
+| UsingSplit | 1,197.6 ns | 11.929 ns | 25.933 ns | 0.9251 | 1456 B |
+| UsingSpanWithStringBuilder | 1,542.0 ns | 15.249 ns | 33.792 ns | 0.9460 | 1488 B |
+| UsingSpanWithNoAlloc | 272.6 ns | 2.265 ns | 5.018 ns | 0.0863 | 136 B |
diff --git a/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.RequestMiddleware.ContainsMediaTypeParameters_Benchmarks-report-github.md b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.RequestMiddleware.ContainsMediaTypeParameters_Benchmarks-report-github.md
new file mode 100644
index 0000000000..066e7b2036
--- /dev/null
+++ b/benchmarks/BenchmarkDotNet.Artifacts/results/Benchmarks.RequestMiddleware.ContainsMediaTypeParameters_Benchmarks-report-github.md
@@ -0,0 +1,14 @@
+``` ini
+
+BenchmarkDotNet=v0.10.10, OS=Mac OS X 10.12
+Processor=Intel Core i5-5257U CPU 2.70GHz (Broadwell), ProcessorCount=4
+.NET Core SDK=2.1.4
+ [Host] : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT
+ DefaultJob : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT
+
+
+```
+| Method | Mean | Error | StdDev | Gen 0 | Allocated |
+|----------- |----------:|----------:|----------:|-------:|----------:|
+| UsingSplit | 157.28 ns | 2.9689 ns | 5.8602 ns | 0.2134 | 336 B |
+| Current | 39.96 ns | 0.6489 ns | 0.6070 ns | - | 0 B |
diff --git a/benchmarks/JsonApiContext/PathIsRelationship_Benchmarks.cs b/benchmarks/JsonApiContext/PathIsRelationship_Benchmarks.cs
new file mode 100644
index 0000000000..83fe6fc53c
--- /dev/null
+++ b/benchmarks/JsonApiContext/PathIsRelationship_Benchmarks.cs
@@ -0,0 +1,24 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Attributes.Exporters;
+
+namespace Benchmarks.JsonApiContext
+{
+ [MarkdownExporter, MemoryDiagnoser]
+ public class PathIsRelationship_Benchmarks
+ {
+ private const string PATH = "https://example.com/api/v1/namespace/articles/relationships/author/";
+
+ [Benchmark]
+ public void Current()
+ => JsonApiDotNetCore.Services.JsonApiContext.PathIsRelationship(PATH);
+
+ [Benchmark]
+ public void UsingSplit() => UsingSplitImpl(PATH);
+
+ private bool UsingSplitImpl(string path)
+ {
+ var split = path.Split('/');
+ return split[split.Length - 2] == "relationships";
+ }
+ }
+}
diff --git a/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs b/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs
new file mode 100644
index 0000000000..05728321c3
--- /dev/null
+++ b/benchmarks/LinkBuilder/LinkBuilder_ GetNamespaceFromPath_Benchmarks.cs
@@ -0,0 +1,38 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Attributes.Exporters;
+using BenchmarkDotNet.Attributes.Jobs;
+
+namespace Benchmarks.LinkBuilder
+{
+ [MarkdownExporter, SimpleJob(launchCount : 3, warmupCount : 10, targetCount : 20), MemoryDiagnoser]
+ public class LinkBuilder_GetNamespaceFromPath_Benchmarks
+ {
+ private const string PATH = "/api/some-really-long-namespace-path/resources/current/articles";
+ private const string ENTITY_NAME = "articles";
+
+ [Benchmark]
+ public void UsingSplit() => GetNamespaceFromPath_BySplitting(PATH, ENTITY_NAME);
+
+ [Benchmark]
+ public void Current() => GetNameSpaceFromPath_Current(PATH, ENTITY_NAME);
+
+ public static string GetNamespaceFromPath_BySplitting(string path, string entityName)
+ {
+ var nSpace = string.Empty;
+ var segments = path.Split('/');
+
+ for (var i = 1; i < segments.Length; i++)
+ {
+ if (segments[i].ToLower() == entityName)
+ break;
+
+ nSpace += $"/{segments[i]}";
+ }
+
+ return nSpace;
+ }
+
+ public static string GetNameSpaceFromPath_Current(string path, string entityName)
+ => JsonApiDotNetCore.Builders.LinkBuilder.GetNamespaceFromPath(path, entityName);
+ }
+}
diff --git a/benchmarks/Program.cs b/benchmarks/Program.cs
index 7665d5fb97..9a2c45dffb 100644
--- a/benchmarks/Program.cs
+++ b/benchmarks/Program.cs
@@ -1,5 +1,8 @@
using BenchmarkDotNet.Running;
+using Benchmarks.JsonApiContext;
+using Benchmarks.LinkBuilder;
using Benchmarks.Query;
+using Benchmarks.RequestMiddleware;
using Benchmarks.Serialization;
namespace Benchmarks {
@@ -8,7 +11,10 @@ static void Main(string[] args) {
var switcher = new BenchmarkSwitcher(new[] {
typeof(JsonApiDeserializer_Benchmarks),
typeof(JsonApiSerializer_Benchmarks),
- typeof(QueryParser_Benchmarks)
+ typeof(QueryParser_Benchmarks),
+ typeof(LinkBuilder_GetNamespaceFromPath_Benchmarks),
+ typeof(ContainsMediaTypeParameters_Benchmarks),
+ typeof(PathIsRelationship_Benchmarks)
});
switcher.Run(args);
}
diff --git a/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs b/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs
new file mode 100644
index 0000000000..ed64c98335
--- /dev/null
+++ b/benchmarks/RequestMiddleware/ContainsMediaTypeParameters_Benchmarks.cs
@@ -0,0 +1,25 @@
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Attributes.Exporters;
+using JsonApiDotNetCore.Internal;
+
+namespace Benchmarks.RequestMiddleware
+{
+ [MarkdownExporter, MemoryDiagnoser]
+ public class ContainsMediaTypeParameters_Benchmarks
+ {
+ private const string MEDIA_TYPE = "application/vnd.api+json; version=1";
+
+ [Benchmark]
+ public void UsingSplit() => UsingSplitImpl(MEDIA_TYPE);
+
+ [Benchmark]
+ public void Current()
+ => JsonApiDotNetCore.Middleware.RequestMiddleware.ContainsMediaTypeParameters(MEDIA_TYPE);
+
+ private bool UsingSplitImpl(string mediaType)
+ {
+ var mediaTypeArr = mediaType.Split(';');
+ return (mediaTypeArr[0] == Constants.ContentType && mediaTypeArr.Length == 2);
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs
index dced1225a9..c3769eccbc 100644
--- a/src/JsonApiDotNetCore/Builders/LinkBuilder.cs
+++ b/src/JsonApiDotNetCore/Builders/LinkBuilder.cs
@@ -1,3 +1,4 @@
+using System;
using JsonApiDotNetCore.Services;
using Microsoft.AspNetCore.Http;
@@ -16,24 +17,39 @@ public string GetBasePath(HttpContext context, string entityName)
{
var r = context.Request;
return (_context.Options.RelativeLinks)
- ? $"{GetNamespaceFromPath(r.Path, entityName)}"
+ ? GetNamespaceFromPath(r.Path, entityName)
: $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}";
}
- private string GetNamespaceFromPath(string path, string entityName)
+ internal static string GetNamespaceFromPath(string path, string entityName)
{
- var nSpace = string.Empty;
- var segments = path.Split('/');
-
- for (var i = 1; i < segments.Length; i++)
+ var entityNameSpan = entityName.AsSpan();
+ var pathSpan = path.AsSpan();
+ const char delimiter = '/';
+ for (var i = 0; i < pathSpan.Length; i++)
{
- if (segments[i].ToLower() == entityName)
- break;
+ if(pathSpan[i].Equals(delimiter))
+ {
+ var nextPosition = i + 1;
+ if(pathSpan.Length > i + entityNameSpan.Length)
+ {
+ var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length);
+ if (entityNameSpan.SequenceEqual(possiblePathSegment))
+ {
+ // check to see if it's the last position in the string
+ // or if the next character is a /
+ var lastCharacterPosition = nextPosition + entityNameSpan.Length;
- nSpace += $"/{segments[i]}";
+ if(lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition].Equals(delimiter))
+ {
+ return pathSpan.Slice(0, i).ToString();
+ }
+ }
+ }
+ }
}
- return nSpace;
+ return string.Empty;
}
public string GetSelfRelationLink(string parent, string parentId, string child)
diff --git a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs
index 5ec658873d..d567de200a 100644
--- a/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs
+++ b/src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using JsonApiDotNetCore.Extensions;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Services;
@@ -8,21 +9,19 @@ namespace JsonApiDotNetCore.Internal.Query
public class RelatedAttrFilterQuery : BaseFilterQuery
{
private readonly IJsonApiContext _jsonApiContext;
-
+
public RelatedAttrFilterQuery(
- IJsonApiContext jsonApiCopntext,
+ IJsonApiContext jsonApiContext,
FilterQuery filterQuery)
{
- _jsonApiContext = jsonApiCopntext;
+ _jsonApiContext = jsonApiContext;
var relationshipArray = filterQuery.Attribute.Split('.');
-
var relationship = GetRelationship(relationshipArray[0]);
if (relationship == null)
throw new JsonApiException(400, $"{relationshipArray[1]} is not a valid relationship on {relationshipArray[0]}.");
var attribute = GetAttribute(relationship, relationshipArray[1]);
-
if (attribute == null)
throw new JsonApiException(400, $"'{filterQuery.Attribute}' is not a valid attribute.");
diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
index c4b9a6d932..f13c10ead7 100755
--- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
+++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
@@ -21,6 +21,7 @@
+
@@ -31,6 +32,12 @@
bin\Release\netstandard2.0\JsonApiDotNetCore.xml
+
+ 7.2
+
+
+ 7.2
+
diff --git a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs
index 6e2612c9a6..0ce54c8589 100644
--- a/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs
+++ b/src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs
@@ -1,3 +1,4 @@
+using System;
using System.Threading.Tasks;
using JsonApiDotNetCore.Internal;
using Microsoft.AspNetCore.Http;
@@ -52,10 +53,23 @@ private static bool IsValidAcceptHeader(HttpContext context)
return true;
}
- private static bool ContainsMediaTypeParameters(string mediaType)
+ internal static bool ContainsMediaTypeParameters(string mediaType)
{
- var mediaTypeArr = mediaType.Split(';');
- return (mediaTypeArr[0] == Constants.ContentType && mediaTypeArr.Length == 2);
+ var incomingMediaTypeSpan = mediaType.AsSpan();
+
+ // if the content type is not application/vnd.api+json then continue on
+ if(incomingMediaTypeSpan.Length < Constants.ContentType.Length)
+ return false;
+
+ var incomingContentType = incomingMediaTypeSpan.Slice(0, Constants.ContentType.Length);
+ if(incomingContentType.SequenceEqual(Constants.ContentType.AsSpan()) == false)
+ return false;
+
+ // anything appended to "application/vnd.api+json;" will be considered a media type param
+ return (
+ incomingMediaTypeSpan.Length >= Constants.ContentType.Length + 2
+ && incomingMediaTypeSpan[Constants.ContentType.Length] == ';'
+ );
}
private static void FlushResponse(HttpContext context, int statusCode)
diff --git a/src/JsonApiDotNetCore/Services/JsonApiContext.cs b/src/JsonApiDotNetCore/Services/JsonApiContext.cs
index 1ebf5aeea1..2665217fef 100644
--- a/src/JsonApiDotNetCore/Services/JsonApiContext.cs
+++ b/src/JsonApiDotNetCore/Services/JsonApiContext.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using JsonApiDotNetCore.Builders;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Internal;
@@ -64,7 +63,6 @@ public IJsonApiContext ApplyContext(object controller)
throw new JsonApiException(500, $"A resource has not been properly defined for type '{typeof(T)}'. Ensure it has been registered on the ContextGraph.");
var context = _httpContextAccessor.HttpContext;
- var path = context.Request.Path.Value.Split('/');
if (context.Request.Query.Count > 0)
{
@@ -72,13 +70,47 @@ public IJsonApiContext ApplyContext(object controller)
IncludedRelationships = QuerySet.IncludedRelationships;
}
- var linkBuilder = new LinkBuilder(this);
- BasePath = linkBuilder.GetBasePath(context, _controllerContext.RequestEntity.EntityName);
+ BasePath = new LinkBuilder(this).GetBasePath(context, _controllerContext.RequestEntity.EntityName);
PageManager = GetPageManager();
- IsRelationshipPath = path[path.Length - 2] == "relationships";
+ IsRelationshipPath = PathIsRelationship(context.Request.Path.Value);
+
return this;
}
+ internal static bool PathIsRelationship(string requestPath)
+ {
+ // while(!Debugger.IsAttached) { Thread.Sleep(1000); }
+ const string relationships = "relationships";
+ const char pathSegmentDelimiter = '/';
+
+ var span = requestPath.AsSpan();
+
+ // we need to iterate over the string, from the end,
+ // checking whether or not the 2nd to last path segment
+ // is "relationships"
+ // -2 is chosen in case the path ends with '/'
+ for(var i = requestPath.Length - 2; i >= 0; i--)
+ {
+ // if there are not enough characters left in the path to
+ // contain "relationships"
+ if(i < relationships.Length)
+ return false;
+
+ // we have found the first instance of '/'
+ if(span[i] == pathSegmentDelimiter)
+ {
+ // in the case of a "relationships" route, the next
+ // path segment will be "relationships"
+ return (
+ span.Slice(i - relationships.Length, relationships.Length)
+ .SequenceEqual(relationships.AsSpan())
+ );
+ }
+ }
+
+ return false;
+ }
+
private PageManager GetPageManager()
{
if (Options.DefaultPageSize == 0 && (QuerySet == null || QuerySet.PageQuery.PageSize == 0))
diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs
index 5e705f4bc9..34bf525e8e 100644
--- a/src/JsonApiDotNetCore/Services/QueryParser.cs
+++ b/src/JsonApiDotNetCore/Services/QueryParser.cs
@@ -235,5 +235,11 @@ protected virtual AttrAttribute GetAttribute(string propertyName)
throw new JsonApiException(400, $"Attribute '{propertyName}' does not exist on resource '{_controllerContext.RequestEntity.EntityName}'", e);
}
}
+
+ private FilterQuery BuildFilterQuery(ReadOnlySpan query, string propertyName)
+ {
+ var (operation, filterValue) = ParseFilterOperation(query.ToString());
+ return new FilterQuery(propertyName, filterValue, operation);
+ }
}
}