Skip to content

Commit cee11aa

Browse files
authored
Merge pull request #271 from crfloyd/feature/#258
Feature/#258
2 parents 015938f + 369860c commit cee11aa

14 files changed

+234
-25
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ services:
66
before_script:
77
- psql -c 'create database JsonApiDotNetCoreExample;' -U postgres
88
mono: none
9-
dotnet: 2.0.3 # https://www.microsoft.com/net/download/linux
9+
dotnet: 2.1.105 # https://www.microsoft.com/net/download/linux
1010
branches:
1111
only:
1212
- master
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
```ini
2+
BenchmarkDotNet=v0.10.10, OS=Mac OS X 10.12
3+
Processor=Intel Core i5-5257U CPU 2.70GHz (Broadwell), ProcessorCount=4
4+
.NET Core SDK=2.1.4
5+
[Host] : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT
6+
DefaultJob : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT
7+
```
8+
9+
| Method | Mean | Error | StdDev | Gen 0 | Allocated |
10+
| ---------- | --------: | ---------: | ---------: | -----: | --------: |
11+
| UsingSplit | 421.08 ns | 19.3905 ns | 54.0529 ns | 0.4725 | 744 B |
12+
| Current | 52.23 ns | 0.8052 ns | 0.7532 ns | - | 0 B |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
``` ini
2+
3+
BenchmarkDotNet=v0.10.10, OS=Mac OS X 10.12
4+
Processor=Intel Core i5-5257U CPU 2.70GHz (Broadwell), ProcessorCount=4
5+
.NET Core SDK=2.1.4
6+
[Host] : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT
7+
Job-XFMVNE : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT
8+
9+
LaunchCount=3 TargetCount=20 WarmupCount=10
10+
11+
```
12+
| Method | Mean | Error | StdDev | Gen 0 | Allocated |
13+
|--------------------------- |-----------:|----------:|----------:|-------:|----------:|
14+
| UsingSplit | 1,197.6 ns | 11.929 ns | 25.933 ns | 0.9251 | 1456 B |
15+
| UsingSpanWithStringBuilder | 1,542.0 ns | 15.249 ns | 33.792 ns | 0.9460 | 1488 B |
16+
| UsingSpanWithNoAlloc | 272.6 ns | 2.265 ns | 5.018 ns | 0.0863 | 136 B |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
``` ini
2+
3+
BenchmarkDotNet=v0.10.10, OS=Mac OS X 10.12
4+
Processor=Intel Core i5-5257U CPU 2.70GHz (Broadwell), ProcessorCount=4
5+
.NET Core SDK=2.1.4
6+
[Host] : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT
7+
DefaultJob : .NET Core 2.0.5 (Framework 4.6.0.0), 64bit RyuJIT
8+
9+
10+
```
11+
| Method | Mean | Error | StdDev | Gen 0 | Allocated |
12+
|----------- |----------:|----------:|----------:|-------:|----------:|
13+
| UsingSplit | 157.28 ns | 2.9689 ns | 5.8602 ns | 0.2134 | 336 B |
14+
| Current | 39.96 ns | 0.6489 ns | 0.6070 ns | - | 0 B |
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using BenchmarkDotNet.Attributes;
2+
using BenchmarkDotNet.Attributes.Exporters;
3+
4+
namespace Benchmarks.JsonApiContext
5+
{
6+
[MarkdownExporter, MemoryDiagnoser]
7+
public class PathIsRelationship_Benchmarks
8+
{
9+
private const string PATH = "https://example.com/api/v1/namespace/articles/relationships/author/";
10+
11+
[Benchmark]
12+
public void Current()
13+
=> JsonApiDotNetCore.Services.JsonApiContext.PathIsRelationship(PATH);
14+
15+
[Benchmark]
16+
public void UsingSplit() => UsingSplitImpl(PATH);
17+
18+
private bool UsingSplitImpl(string path)
19+
{
20+
var split = path.Split('/');
21+
return split[split.Length - 2] == "relationships";
22+
}
23+
}
24+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using BenchmarkDotNet.Attributes;
2+
using BenchmarkDotNet.Attributes.Exporters;
3+
using BenchmarkDotNet.Attributes.Jobs;
4+
5+
namespace Benchmarks.LinkBuilder
6+
{
7+
[MarkdownExporter, SimpleJob(launchCount : 3, warmupCount : 10, targetCount : 20), MemoryDiagnoser]
8+
public class LinkBuilder_GetNamespaceFromPath_Benchmarks
9+
{
10+
private const string PATH = "/api/some-really-long-namespace-path/resources/current/articles";
11+
private const string ENTITY_NAME = "articles";
12+
13+
[Benchmark]
14+
public void UsingSplit() => GetNamespaceFromPath_BySplitting(PATH, ENTITY_NAME);
15+
16+
[Benchmark]
17+
public void Current() => GetNameSpaceFromPath_Current(PATH, ENTITY_NAME);
18+
19+
public static string GetNamespaceFromPath_BySplitting(string path, string entityName)
20+
{
21+
var nSpace = string.Empty;
22+
var segments = path.Split('/');
23+
24+
for (var i = 1; i < segments.Length; i++)
25+
{
26+
if (segments[i].ToLower() == entityName)
27+
break;
28+
29+
nSpace += $"/{segments[i]}";
30+
}
31+
32+
return nSpace;
33+
}
34+
35+
public static string GetNameSpaceFromPath_Current(string path, string entityName)
36+
=> JsonApiDotNetCore.Builders.LinkBuilder.GetNamespaceFromPath(path, entityName);
37+
}
38+
}

benchmarks/Program.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using BenchmarkDotNet.Running;
2+
using Benchmarks.JsonApiContext;
3+
using Benchmarks.LinkBuilder;
24
using Benchmarks.Query;
5+
using Benchmarks.RequestMiddleware;
36
using Benchmarks.Serialization;
47

58
namespace Benchmarks {
@@ -8,7 +11,10 @@ static void Main(string[] args) {
811
var switcher = new BenchmarkSwitcher(new[] {
912
typeof(JsonApiDeserializer_Benchmarks),
1013
typeof(JsonApiSerializer_Benchmarks),
11-
typeof(QueryParser_Benchmarks)
14+
typeof(QueryParser_Benchmarks),
15+
typeof(LinkBuilder_GetNamespaceFromPath_Benchmarks),
16+
typeof(ContainsMediaTypeParameters_Benchmarks),
17+
typeof(PathIsRelationship_Benchmarks)
1218
});
1319
switcher.Run(args);
1420
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using BenchmarkDotNet.Attributes;
2+
using BenchmarkDotNet.Attributes.Exporters;
3+
using JsonApiDotNetCore.Internal;
4+
5+
namespace Benchmarks.RequestMiddleware
6+
{
7+
[MarkdownExporter, MemoryDiagnoser]
8+
public class ContainsMediaTypeParameters_Benchmarks
9+
{
10+
private const string MEDIA_TYPE = "application/vnd.api+json; version=1";
11+
12+
[Benchmark]
13+
public void UsingSplit() => UsingSplitImpl(MEDIA_TYPE);
14+
15+
[Benchmark]
16+
public void Current()
17+
=> JsonApiDotNetCore.Middleware.RequestMiddleware.ContainsMediaTypeParameters(MEDIA_TYPE);
18+
19+
private bool UsingSplitImpl(string mediaType)
20+
{
21+
var mediaTypeArr = mediaType.Split(';');
22+
return (mediaTypeArr[0] == Constants.ContentType && mediaTypeArr.Length == 2);
23+
}
24+
}
25+
}

src/JsonApiDotNetCore/Builders/LinkBuilder.cs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using JsonApiDotNetCore.Services;
23
using Microsoft.AspNetCore.Http;
34

@@ -16,24 +17,39 @@ public string GetBasePath(HttpContext context, string entityName)
1617
{
1718
var r = context.Request;
1819
return (_context.Options.RelativeLinks)
19-
? $"{GetNamespaceFromPath(r.Path, entityName)}"
20+
? GetNamespaceFromPath(r.Path, entityName)
2021
: $"{r.Scheme}://{r.Host}{GetNamespaceFromPath(r.Path, entityName)}";
2122
}
2223

23-
private string GetNamespaceFromPath(string path, string entityName)
24+
internal static string GetNamespaceFromPath(string path, string entityName)
2425
{
25-
var nSpace = string.Empty;
26-
var segments = path.Split('/');
27-
28-
for (var i = 1; i < segments.Length; i++)
26+
var entityNameSpan = entityName.AsSpan();
27+
var pathSpan = path.AsSpan();
28+
const char delimiter = '/';
29+
for (var i = 0; i < pathSpan.Length; i++)
2930
{
30-
if (segments[i].ToLower() == entityName)
31-
break;
31+
if(pathSpan[i].Equals(delimiter))
32+
{
33+
var nextPosition = i + 1;
34+
if(pathSpan.Length > i + entityNameSpan.Length)
35+
{
36+
var possiblePathSegment = pathSpan.Slice(nextPosition, entityNameSpan.Length);
37+
if (entityNameSpan.SequenceEqual(possiblePathSegment))
38+
{
39+
// check to see if it's the last position in the string
40+
// or if the next character is a /
41+
var lastCharacterPosition = nextPosition + entityNameSpan.Length;
3242

33-
nSpace += $"/{segments[i]}";
43+
if(lastCharacterPosition == pathSpan.Length || pathSpan.Length >= lastCharacterPosition + 2 && pathSpan[lastCharacterPosition].Equals(delimiter))
44+
{
45+
return pathSpan.Slice(0, i).ToString();
46+
}
47+
}
48+
}
49+
}
3450
}
3551

36-
return nSpace;
52+
return string.Empty;
3753
}
3854

3955
public string GetSelfRelationLink(string parent, string parentId, string child)

src/JsonApiDotNetCore/Internal/Query/RelatedAttrFilterQuery.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Linq;
3+
using JsonApiDotNetCore.Extensions;
34
using JsonApiDotNetCore.Models;
45
using JsonApiDotNetCore.Services;
56

@@ -8,21 +9,19 @@ namespace JsonApiDotNetCore.Internal.Query
89
public class RelatedAttrFilterQuery : BaseFilterQuery
910
{
1011
private readonly IJsonApiContext _jsonApiContext;
11-
12+
1213
public RelatedAttrFilterQuery(
13-
IJsonApiContext jsonApiCopntext,
14+
IJsonApiContext jsonApiContext,
1415
FilterQuery filterQuery)
1516
{
16-
_jsonApiContext = jsonApiCopntext;
17+
_jsonApiContext = jsonApiContext;
1718

1819
var relationshipArray = filterQuery.Attribute.Split('.');
19-
2020
var relationship = GetRelationship(relationshipArray[0]);
2121
if (relationship == null)
2222
throw new JsonApiException(400, $"{relationshipArray[1]} is not a valid relationship on {relationshipArray[0]}.");
2323

2424
var attribute = GetAttribute(relationship, relationshipArray[1]);
25-
2625
if (attribute == null)
2726
throw new JsonApiException(400, $"'{filterQuery.Attribute}' is not a valid attribute.");
2827

src/JsonApiDotNetCore/JsonApiDotNetCore.csproj

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(AspNetCoreVersion)" />
2222
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="$(EFCoreVersion)" />
2323
<PackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftLoggingVersion)" />
24+
<PackageReference Include="System.Memory" Version="4.5.0-preview2-26406-04" />
2425
<PackageReference Include="System.ValueTuple" Version="$(TuplesVersion)" />
2526
</ItemGroup>
2627

@@ -31,6 +32,12 @@
3132
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
3233
<DocumentationFile>bin\Release\netstandard2.0\JsonApiDotNetCore.xml</DocumentationFile>
3334
</PropertyGroup>
35+
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|netstandard2.0|AnyCPU'">
36+
<LangVersion>7.2</LangVersion>
37+
</PropertyGroup>
38+
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|netstandard2.0|AnyCPU'">
39+
<LangVersion>7.2</LangVersion>
40+
</PropertyGroup>
3441
<ItemGroup Condition="$(IsWindows)=='true'">
3542
<PackageReference Include="docfx.console" Version="2.33.0" />
3643
</ItemGroup>

src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Threading.Tasks;
23
using JsonApiDotNetCore.Internal;
34
using Microsoft.AspNetCore.Http;
@@ -52,10 +53,23 @@ private static bool IsValidAcceptHeader(HttpContext context)
5253
return true;
5354
}
5455

55-
private static bool ContainsMediaTypeParameters(string mediaType)
56+
internal static bool ContainsMediaTypeParameters(string mediaType)
5657
{
57-
var mediaTypeArr = mediaType.Split(';');
58-
return (mediaTypeArr[0] == Constants.ContentType && mediaTypeArr.Length == 2);
58+
var incomingMediaTypeSpan = mediaType.AsSpan();
59+
60+
// if the content type is not application/vnd.api+json then continue on
61+
if(incomingMediaTypeSpan.Length < Constants.ContentType.Length)
62+
return false;
63+
64+
var incomingContentType = incomingMediaTypeSpan.Slice(0, Constants.ContentType.Length);
65+
if(incomingContentType.SequenceEqual(Constants.ContentType.AsSpan()) == false)
66+
return false;
67+
68+
// anything appended to "application/vnd.api+json;" will be considered a media type param
69+
return (
70+
incomingMediaTypeSpan.Length >= Constants.ContentType.Length + 2
71+
&& incomingMediaTypeSpan[Constants.ContentType.Length] == ';'
72+
);
5973
}
6074

6175
private static void FlushResponse(HttpContext context, int statusCode)

src/JsonApiDotNetCore/Services/JsonApiContext.cs

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Linq;
43
using JsonApiDotNetCore.Builders;
54
using JsonApiDotNetCore.Configuration;
65
using JsonApiDotNetCore.Internal;
@@ -64,21 +63,54 @@ public IJsonApiContext ApplyContext<T>(object controller)
6463
throw new JsonApiException(500, $"A resource has not been properly defined for type '{typeof(T)}'. Ensure it has been registered on the ContextGraph.");
6564

6665
var context = _httpContextAccessor.HttpContext;
67-
var path = context.Request.Path.Value.Split('/');
6866

6967
if (context.Request.Query.Count > 0)
7068
{
7169
QuerySet = _queryParser.Parse(context.Request.Query);
7270
IncludedRelationships = QuerySet.IncludedRelationships;
7371
}
7472

75-
var linkBuilder = new LinkBuilder(this);
76-
BasePath = linkBuilder.GetBasePath(context, _controllerContext.RequestEntity.EntityName);
73+
BasePath = new LinkBuilder(this).GetBasePath(context, _controllerContext.RequestEntity.EntityName);
7774
PageManager = GetPageManager();
78-
IsRelationshipPath = path[path.Length - 2] == "relationships";
75+
IsRelationshipPath = PathIsRelationship(context.Request.Path.Value);
76+
7977
return this;
8078
}
8179

80+
internal static bool PathIsRelationship(string requestPath)
81+
{
82+
// while(!Debugger.IsAttached) { Thread.Sleep(1000); }
83+
const string relationships = "relationships";
84+
const char pathSegmentDelimiter = '/';
85+
86+
var span = requestPath.AsSpan();
87+
88+
// we need to iterate over the string, from the end,
89+
// checking whether or not the 2nd to last path segment
90+
// is "relationships"
91+
// -2 is chosen in case the path ends with '/'
92+
for(var i = requestPath.Length - 2; i >= 0; i--)
93+
{
94+
// if there are not enough characters left in the path to
95+
// contain "relationships"
96+
if(i < relationships.Length)
97+
return false;
98+
99+
// we have found the first instance of '/'
100+
if(span[i] == pathSegmentDelimiter)
101+
{
102+
// in the case of a "relationships" route, the next
103+
// path segment will be "relationships"
104+
return (
105+
span.Slice(i - relationships.Length, relationships.Length)
106+
.SequenceEqual(relationships.AsSpan())
107+
);
108+
}
109+
}
110+
111+
return false;
112+
}
113+
82114
private PageManager GetPageManager()
83115
{
84116
if (Options.DefaultPageSize == 0 && (QuerySet == null || QuerySet.PageQuery.PageSize == 0))

src/JsonApiDotNetCore/Services/QueryParser.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,5 +235,11 @@ protected virtual AttrAttribute GetAttribute(string propertyName)
235235
throw new JsonApiException(400, $"Attribute '{propertyName}' does not exist on resource '{_controllerContext.RequestEntity.EntityName}'", e);
236236
}
237237
}
238+
239+
private FilterQuery BuildFilterQuery(ReadOnlySpan<char> query, string propertyName)
240+
{
241+
var (operation, filterValue) = ParseFilterOperation(query.ToString());
242+
return new FilterQuery(propertyName, filterValue, operation);
243+
}
238244
}
239245
}

0 commit comments

Comments
 (0)