Skip to content

Commit 449e3f8

Browse files
committed
Merge branch 'fix/#237' into develop
2 parents a6aebe4 + d76c600 commit 449e3f8

32 files changed

+1427
-474
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/DocumentBuilder.cs

Lines changed: 58 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ public class DocumentBuilder : IDocumentBuilder
1313
private readonly IJsonApiContext _jsonApiContext;
1414
private readonly IContextGraph _contextGraph;
1515
private readonly IRequestMeta _requestMeta;
16-
private readonly DocumentBuilderOptions _documentBuilderOptions;
16+
private readonly DocumentBuilderOptions _documentBuilderOptions;
1717

18-
public DocumentBuilder(IJsonApiContext jsonApiContext, IRequestMeta requestMeta=null, IDocumentBuilderOptionsProvider documentBuilderOptionsProvider=null)
18+
public DocumentBuilder(IJsonApiContext jsonApiContext, IRequestMeta requestMeta = null, IDocumentBuilderOptionsProvider documentBuilderOptionsProvider = null)
1919
{
2020
_jsonApiContext = jsonApiContext;
2121
_contextGraph = jsonApiContext.ContextGraph;
@@ -107,7 +107,7 @@ public DocumentData GetData(ContextEntity contextEntity, IIdentifiable entity)
107107
Id = entity.StringId
108108
};
109109

110-
if (_jsonApiContext.IsRelationshipData)
110+
if (_jsonApiContext.IsRelationshipPath)
111111
return data;
112112

113113
data.Attributes = new Dictionary<string, object>();
@@ -143,37 +143,42 @@ private bool OmitNullValuedAttribute(AttrAttribute attr, object attributeValue)
143143
private void AddRelationships(DocumentData data, ContextEntity contextEntity, IIdentifiable entity)
144144
{
145145
data.Relationships = new Dictionary<string, RelationshipData>();
146-
var linkBuilder = new LinkBuilder(_jsonApiContext);
147-
148146
contextEntity.Relationships.ForEach(r =>
149-
{
150-
var relationshipData = new RelationshipData();
147+
data.Relationships.Add(
148+
r.PublicRelationshipName,
149+
GetRelationshipData(r, contextEntity, entity)
150+
)
151+
);
152+
}
151153

152-
if (r.DocumentLinks.HasFlag(Link.None) == false)
153-
{
154-
relationshipData.Links = new Links();
155-
if (r.DocumentLinks.HasFlag(Link.Self))
156-
relationshipData.Links.Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.StringId, r.PublicRelationshipName);
154+
private RelationshipData GetRelationshipData(RelationshipAttribute attr, ContextEntity contextEntity, IIdentifiable entity)
155+
{
156+
var linkBuilder = new LinkBuilder(_jsonApiContext);
157157

158-
if (r.DocumentLinks.HasFlag(Link.Related))
159-
relationshipData.Links.Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.StringId, r.PublicRelationshipName);
160-
}
158+
var relationshipData = new RelationshipData();
161159

162-
if (RelationshipIsIncluded(r.PublicRelationshipName))
163-
{
164-
var navigationEntity = _jsonApiContext.ContextGraph
165-
.GetRelationship(entity, r.InternalRelationshipName);
166-
167-
if (navigationEntity == null)
168-
relationshipData.SingleData = null;
169-
else if (navigationEntity is IEnumerable)
170-
relationshipData.ManyData = GetRelationships((IEnumerable<object>)navigationEntity);
171-
else
172-
relationshipData.SingleData = GetRelationship(navigationEntity);
173-
}
160+
if (attr.DocumentLinks.HasFlag(Link.None) == false)
161+
{
162+
relationshipData.Links = new Links();
163+
if (attr.DocumentLinks.HasFlag(Link.Self))
164+
relationshipData.Links.Self = linkBuilder.GetSelfRelationLink(contextEntity.EntityName, entity.StringId, attr.PublicRelationshipName);
174165

175-
data.Relationships.Add(r.PublicRelationshipName, relationshipData);
176-
});
166+
if (attr.DocumentLinks.HasFlag(Link.Related))
167+
relationshipData.Links.Related = linkBuilder.GetRelatedRelationLink(contextEntity.EntityName, entity.StringId, attr.PublicRelationshipName);
168+
}
169+
170+
// this only includes the navigation property, we need to actually check the navigation property Id
171+
var navigationEntity = _jsonApiContext.ContextGraph.GetRelationship(entity, attr.InternalRelationshipName);
172+
if (navigationEntity == null)
173+
relationshipData.SingleData = attr.IsHasOne
174+
? GetIndependentRelationshipIdentifier((HasOneAttribute)attr, entity)
175+
: null;
176+
else if (navigationEntity is IEnumerable)
177+
relationshipData.ManyData = GetRelationships((IEnumerable<object>)navigationEntity);
178+
else
179+
relationshipData.SingleData = GetRelationship(navigationEntity);
180+
181+
return relationshipData;
177182
}
178183

179184
private List<DocumentData> GetIncludedEntities(List<DocumentData> included, ContextEntity contextEntity, IIdentifiable entity)
@@ -243,23 +248,42 @@ private List<ResourceIdentifierObject> GetRelationships(IEnumerable<object> enti
243248
var relationships = new List<ResourceIdentifierObject>();
244249
foreach (var entity in entities)
245250
{
246-
relationships.Add(new ResourceIdentifierObject {
251+
relationships.Add(new ResourceIdentifierObject
252+
{
247253
Type = typeName.EntityName,
248254
Id = ((IIdentifiable)entity).StringId
249255
});
250256
}
251257
return relationships;
252258
}
259+
253260
private ResourceIdentifierObject GetRelationship(object entity)
254261
{
255262
var objType = entity.GetType();
263+
var contextEntity = _jsonApiContext.ContextGraph.GetContextEntity(objType);
256264

257-
var typeName = _jsonApiContext.ContextGraph.GetContextEntity(objType);
258-
259-
return new ResourceIdentifierObject {
260-
Type = typeName.EntityName,
265+
return new ResourceIdentifierObject
266+
{
267+
Type = contextEntity.EntityName,
261268
Id = ((IIdentifiable)entity).StringId
262269
};
263270
}
271+
272+
private ResourceIdentifierObject GetIndependentRelationshipIdentifier(HasOneAttribute hasOne, IIdentifiable entity)
273+
{
274+
var independentRelationshipIdentifier = hasOne.GetIdentifiablePropertyValue(entity);
275+
if (independentRelationshipIdentifier == null)
276+
return null;
277+
278+
var relatedContextEntity = _jsonApiContext.ContextGraph.GetContextEntity(hasOne.Type);
279+
if (relatedContextEntity == null) // TODO: this should probably be a debug log at minimum
280+
return null;
281+
282+
return new ResourceIdentifierObject
283+
{
284+
Type = relatedContextEntity.EntityName,
285+
Id = independentRelationshipIdentifier.ToString()
286+
};
287+
}
264288
}
265289
}

src/JsonApiDotNetCore/Builders/LinkBuilder.cs

Lines changed: 29 additions & 11 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)
@@ -48,7 +64,9 @@ public string GetRelatedRelationLink(string parent, string parentId, string chil
4864

4965
public string GetPageLink(int pageOffset, int pageSize)
5066
{
51-
return $"{_context.BasePath}/{_context.RequestEntity.EntityName}?page[size]={pageSize}&page[number]={pageOffset}";
67+
var filterQueryComposer = new QueryComposer();
68+
var filters = filterQueryComposer.Compose(_context);
69+
return $"{_context.BasePath}/{_context.RequestEntity.EntityName}?page[size]={pageSize}&page[number]={pageOffset}{filters}";
5270
}
5371
}
5472
}

0 commit comments

Comments
 (0)