Skip to content

Commit

Permalink
Merge pull request json-api-dotnet#271 from crfloyd/feature/json-api-…
Browse files Browse the repository at this point in the history
  • Loading branch information
jaredcnance authored May 6, 2018
2 parents 015938f + 369860c commit cee11aa
Show file tree
Hide file tree
Showing 14 changed files with 234 additions and 25 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 |
Original file line number Diff line number Diff line change
@@ -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 |
Original file line number Diff line number Diff line change
@@ -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 |
24 changes: 24 additions & 0 deletions benchmarks/JsonApiContext/PathIsRelationship_Benchmarks.cs
Original file line number Diff line number Diff line change
@@ -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";
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
8 changes: 7 additions & 1 deletion benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using BenchmarkDotNet.Running;
using Benchmarks.JsonApiContext;
using Benchmarks.LinkBuilder;
using Benchmarks.Query;
using Benchmarks.RequestMiddleware;
using Benchmarks.Serialization;

namespace Benchmarks {
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
36 changes: 26 additions & 10 deletions src/JsonApiDotNetCore/Builders/LinkBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using JsonApiDotNetCore.Services;
using Microsoft.AspNetCore.Http;

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Linq;
using JsonApiDotNetCore.Extensions;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Services;

Expand All @@ -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.");

Expand Down
7 changes: 7 additions & 0 deletions src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="$(EFCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="$(MicrosoftLoggingVersion)" />
<PackageReference Include="System.Memory" Version="4.5.0-preview2-26406-04" />
<PackageReference Include="System.ValueTuple" Version="$(TuplesVersion)" />
</ItemGroup>

Expand All @@ -31,6 +32,12 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DocumentationFile>bin\Release\netstandard2.0\JsonApiDotNetCore.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|netstandard2.0|AnyCPU'">
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|netstandard2.0|AnyCPU'">
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<ItemGroup Condition="$(IsWindows)=='true'">
<PackageReference Include="docfx.console" Version="2.33.0" />
</ItemGroup>
Expand Down
20 changes: 17 additions & 3 deletions src/JsonApiDotNetCore/Middleware/RequestMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Threading.Tasks;
using JsonApiDotNetCore.Internal;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -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)
Expand Down
42 changes: 37 additions & 5 deletions src/JsonApiDotNetCore/Services/JsonApiContext.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JsonApiDotNetCore.Builders;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Internal;
Expand Down Expand Up @@ -64,21 +63,54 @@ public IJsonApiContext ApplyContext<T>(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)
{
QuerySet = _queryParser.Parse(context.Request.Query);
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))
Expand Down
6 changes: 6 additions & 0 deletions src/JsonApiDotNetCore/Services/QueryParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<char> query, string propertyName)
{
var (operation, filterValue) = ParseFilterOperation(query.ToString());
return new FilterQuery(propertyName, filterValue, operation);
}
}
}

0 comments on commit cee11aa

Please sign in to comment.