Skip to content

Feature/#258 #271

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
}