Skip to content

Commit bf14fe0

Browse files
author
Chris Santero
committed
add materializer for mapping entities to DTOs
1 parent 4d0de76 commit bf14fe0

4 files changed

Lines changed: 243 additions & 1 deletion

File tree

JSONAPI.EntityFramework/Http/EntityFrameworkDocumentMaterializer.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ public virtual Task<IResourceCollectionDocument> GetRecords(HttpRequestMessage r
6363
return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken);
6464
}
6565

66+
public Task<IResourceCollectionDocument> GetRecordsMatchingExpression(Expression<Func<T, bool>> filter, HttpRequestMessage request, CancellationToken cancellationToken)
67+
{
68+
var query = _dbContext.Set<T>().AsQueryable().Where(filter);
69+
return _queryableResourceCollectionDocumentBuilder.BuildDocument(query, request, cancellationToken);
70+
}
71+
6672
public virtual async Task<ISingleResourceDocument> GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken)
6773
{
6874
var apiBaseUrl = GetBaseUrlFromRequest(request);

JSONAPI/Http/IDocumentMaterializer.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using System.Net.Http;
1+
using System;
2+
using System.Linq.Expressions;
3+
using System.Net.Http;
24
using System.Threading;
35
using System.Threading.Tasks;
46
using JSONAPI.Documents;
@@ -16,6 +18,11 @@ public interface IDocumentMaterializer<T> where T : class
1618
/// </summary>
1719
Task<IResourceCollectionDocument> GetRecords(HttpRequestMessage request, CancellationToken cancellationToken);
1820

21+
/// <summary>
22+
/// Returns a document containing records matching the provided lambda expression.
23+
/// </summary>
24+
Task<IResourceCollectionDocument> GetRecordsMatchingExpression(Expression<Func<T, bool>> filter, HttpRequestMessage request, CancellationToken cancellationToken);
25+
1926
/// <summary>
2027
/// Returns a document with the resource identified by the given ID.
2128
/// </summary>
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Linq.Expressions;
6+
using System.Net.Http;
7+
using System.Reflection;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using JSONAPI.Core;
11+
using JSONAPI.Documents;
12+
using JSONAPI.Documents.Builders;
13+
using JSONAPI.QueryableTransformers;
14+
15+
namespace JSONAPI.Http
16+
{
17+
/// <summary>
18+
/// Document materializer for mapping from a database entity to a data transfer object.
19+
/// </summary>
20+
/// <typeparam name="TEntity"></typeparam>
21+
/// <typeparam name="TDto"></typeparam>
22+
public abstract class MappedDocumentMaterializer<TEntity, TDto> : IDocumentMaterializer<TDto> where TDto : class
23+
{
24+
/// <summary>
25+
/// Materializes a document for the resources found on the other side of the to-many relationship belonging to the resource.
26+
/// </summary>
27+
protected delegate Task<IResourceCollectionDocument> MaterializeDocumentForToManyRelationship(
28+
TDto resource, HttpRequestMessage request, CancellationToken cancellationToken);
29+
30+
/// <summary>
31+
/// Materializes a document for the resources found on the other side of the to-one relationship belonging to the resource.
32+
/// </summary>
33+
protected delegate Task<ISingleResourceDocument> MaterializeDocumentForToOneRelationship(
34+
TDto resource, HttpRequestMessage request, CancellationToken cancellationToken);
35+
36+
private readonly IQueryableResourceCollectionDocumentBuilder _queryableResourceCollectionDocumentBuilder;
37+
private readonly IBaseUrlService _baseUrlService;
38+
private readonly ISingleResourceDocumentBuilder _singleResourceDocumentBuilder;
39+
private readonly IQueryableEnumerationTransformer _queryableEnumerationTransformer;
40+
private readonly IResourceTypeRegistry _resourceTypeRegistry;
41+
private readonly IDictionary<ResourceTypeRelationship, MaterializeDocumentForToManyRelationship> _toManyRelatedResourceMaterializers;
42+
private readonly IDictionary<ResourceTypeRelationship, MaterializeDocumentForToOneRelationship> _toOneRelatedResourceMaterializers;
43+
44+
/// <summary>
45+
/// Gets a query returning all entities for this endpoint
46+
/// </summary>
47+
/// <returns></returns>
48+
protected abstract IQueryable<TEntity> GetQuery();
49+
protected abstract IQueryable<TEntity> GetByIdQuery(string id);
50+
protected abstract IQueryable<TDto> GetMappedQuery(IQueryable<TEntity> entityQuery, Expression<Func<TDto, object>>[] propertiesToInclude);
51+
52+
protected MappedDocumentMaterializer(
53+
IQueryableResourceCollectionDocumentBuilder queryableResourceCollectionDocumentBuilder,
54+
IBaseUrlService baseUrlService,
55+
ISingleResourceDocumentBuilder singleResourceDocumentBuilder,
56+
IQueryableEnumerationTransformer queryableEnumerationTransformer,
57+
IResourceTypeRegistry resourceTypeRegistry)
58+
{
59+
_queryableResourceCollectionDocumentBuilder = queryableResourceCollectionDocumentBuilder;
60+
_baseUrlService = baseUrlService;
61+
_singleResourceDocumentBuilder = singleResourceDocumentBuilder;
62+
_queryableEnumerationTransformer = queryableEnumerationTransformer;
63+
_resourceTypeRegistry = resourceTypeRegistry;
64+
_toManyRelatedResourceMaterializers =
65+
new ConcurrentDictionary<ResourceTypeRelationship, MaterializeDocumentForToManyRelationship>();
66+
_toOneRelatedResourceMaterializers =
67+
new ConcurrentDictionary<ResourceTypeRelationship, MaterializeDocumentForToOneRelationship>();
68+
}
69+
70+
public async Task<IResourceCollectionDocument> GetRecords(HttpRequestMessage request, CancellationToken cancellationToken)
71+
{
72+
return await GetRecordsMatchingExpression(m => true, request, cancellationToken);
73+
}
74+
75+
public async Task<IResourceCollectionDocument> GetRecordsMatchingExpression(Expression<Func<TDto, bool>> filter, HttpRequestMessage request, CancellationToken cancellationToken)
76+
{
77+
var entityQuery = GetQuery();
78+
var includePaths = GetIncludePathsForQuery() ?? new Expression<Func<TDto, object>>[] { };
79+
var jsonApiPaths = includePaths.Select(ConvertToJsonKeyPath).ToArray();
80+
var mappedQuery = GetMappedQuery(entityQuery, includePaths);
81+
return await _queryableResourceCollectionDocumentBuilder.BuildDocument(mappedQuery, request, cancellationToken, jsonApiPaths);
82+
}
83+
84+
public async Task<ISingleResourceDocument> GetRecordById(string id, HttpRequestMessage request, CancellationToken cancellationToken)
85+
{
86+
var entityQuery = GetByIdQuery(id);
87+
var includePaths = GetIncludePathsForSingleResource() ?? new Expression<Func<TDto, object>>[] { };
88+
var jsonApiPaths = includePaths.Select(ConvertToJsonKeyPath).ToArray();
89+
var mappedQuery = GetMappedQuery(entityQuery, includePaths);
90+
var primaryResource = await _queryableEnumerationTransformer.FirstOrDefault(mappedQuery, cancellationToken);
91+
if (primaryResource == null) throw JsonApiException.CreateForNotFound(string.Format("No record exists with ID {0} for the requested type.", id));
92+
93+
var baseUrl = _baseUrlService.GetBaseUrl(request);
94+
return _singleResourceDocumentBuilder.BuildDocument(primaryResource, baseUrl, jsonApiPaths);
95+
}
96+
97+
public async Task<IJsonApiDocument> GetRelated(string id, string relationshipKey, HttpRequestMessage request, CancellationToken cancellationToken)
98+
{
99+
var registration = _resourceTypeRegistry.GetRegistrationForType(typeof(TDto));
100+
101+
var primaryEntityQuery = GetByIdQuery(id);
102+
var mappedQuery = GetMappedQuery(primaryEntityQuery, null);
103+
var primaryResource = await _queryableEnumerationTransformer.FirstOrDefault(mappedQuery, cancellationToken);
104+
if (primaryResource == null)
105+
{
106+
var dtoRegistration = _resourceTypeRegistry.GetRegistrationForType(typeof(TDto));
107+
throw JsonApiException.CreateForNotFound(string.Format(
108+
"No resource of type `{0}` exists with id `{1}`.",
109+
dtoRegistration.ResourceTypeName, id));
110+
}
111+
112+
var relationship = (ResourceTypeRelationship)registration.GetFieldByName(relationshipKey);
113+
if (relationship == null)
114+
throw JsonApiException.CreateForNotFound(string.Format("No relationship `{0}` exists for the resource with type `{1}` and id `{2}`.",
115+
relationshipKey, registration.ResourceTypeName, id));
116+
117+
if (relationship.IsToMany)
118+
{
119+
MaterializeDocumentForToManyRelationship documentFactory;
120+
if (!_toManyRelatedResourceMaterializers.TryGetValue(relationship, out documentFactory))
121+
{
122+
documentFactory = GetMaterializerForToManyRelatedResource(relationship);
123+
_toManyRelatedResourceMaterializers.Add(relationship, documentFactory);
124+
}
125+
return await documentFactory(primaryResource, request, cancellationToken);
126+
}
127+
else
128+
{
129+
MaterializeDocumentForToOneRelationship relatedResourceMaterializer;
130+
if (!_toOneRelatedResourceMaterializers.TryGetValue(relationship, out relatedResourceMaterializer))
131+
{
132+
relatedResourceMaterializer = GetMaterializerForToOneRelatedResource(relationship);
133+
_toOneRelatedResourceMaterializers.Add(relationship, relatedResourceMaterializer);
134+
}
135+
return await relatedResourceMaterializer(primaryResource, request, cancellationToken);
136+
}
137+
}
138+
139+
public Task<ISingleResourceDocument> CreateRecord(ISingleResourceDocument requestDocument, HttpRequestMessage request,
140+
CancellationToken cancellationToken)
141+
{
142+
throw new NotImplementedException();
143+
}
144+
145+
public Task<ISingleResourceDocument> UpdateRecord(string id, ISingleResourceDocument requestDocument, HttpRequestMessage request,
146+
CancellationToken cancellationToken)
147+
{
148+
throw new NotImplementedException();
149+
}
150+
151+
public Task<IJsonApiDocument> DeleteRecord(string id, CancellationToken cancellationToken)
152+
{
153+
throw new NotImplementedException();
154+
}
155+
156+
/// <summary>
157+
/// Returns a list of property paths to be included when constructing a query for this resource type
158+
/// </summary>
159+
protected virtual Expression<Func<TDto, object>>[] GetIncludePathsForQuery()
160+
{
161+
return null;
162+
}
163+
164+
/// <summary>
165+
/// Returns a list of property paths to be included when returning a single resource of this resource type
166+
/// </summary>
167+
protected virtual Expression<Func<TDto, object>>[] GetIncludePathsForSingleResource()
168+
{
169+
return null;
170+
}
171+
172+
/// <summary>
173+
/// Returns a materialization delegate to handle related resource requests for a to-many relationship
174+
/// </summary>
175+
protected abstract MaterializeDocumentForToManyRelationship GetMaterializerForToManyRelatedResource(ResourceTypeRelationship relationship);
176+
177+
/// <summary>
178+
/// Returns a materialization delegate to handle related resource requests for a to-one relationship
179+
/// </summary>
180+
protected abstract MaterializeDocumentForToOneRelationship GetMaterializerForToOneRelatedResource(ResourceTypeRelationship relationship);
181+
182+
private string ConvertToJsonKeyPath(Expression<Func<TDto, object>> expression)
183+
{
184+
var visitor = new PathVisitor(_resourceTypeRegistry);
185+
visitor.Visit(expression);
186+
return visitor.Path;
187+
}
188+
189+
private class PathVisitor : ExpressionVisitor
190+
{
191+
private readonly IResourceTypeRegistry _resourceTypeRegistry;
192+
193+
public PathVisitor(IResourceTypeRegistry resourceTypeRegistry)
194+
{
195+
_resourceTypeRegistry = resourceTypeRegistry;
196+
}
197+
198+
private readonly Stack<string> _segments = new Stack<string>();
199+
public string Path { get { return string.Join(".", _segments.ToArray()); } }
200+
201+
protected override Expression VisitMethodCall(MethodCallExpression node)
202+
{
203+
if (node.Method.Name == "Select")
204+
{
205+
Visit(node.Arguments[1]);
206+
Visit(node.Arguments[0]);
207+
}
208+
return node;
209+
}
210+
211+
protected override Expression VisitMember(MemberExpression node)
212+
{
213+
var property = node.Member as PropertyInfo;
214+
if (property == null) return node;
215+
216+
var registration = _resourceTypeRegistry.GetRegistrationForType(property.DeclaringType);
217+
if (registration == null || registration.Relationships == null) return node;
218+
219+
var relationship = registration.Relationships.FirstOrDefault(r => r.Property == property);
220+
if (relationship == null) return node;
221+
222+
_segments.Push(relationship.JsonKey);
223+
224+
return base.VisitMember(node);
225+
}
226+
}
227+
}
228+
}

JSONAPI/JSONAPI.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
<Reference Include="System.Xml" />
6767
</ItemGroup>
6868
<ItemGroup>
69+
<Compile Include="Http\MappedDocumentMaterializer.cs" />
6970
<Compile Include="QueryableTransformers\DefaultFilteringTransformer.cs" />
7071
<Compile Include="QueryableTransformers\DefaultPaginationTransformer.cs" />
7172
<Compile Include="QueryableTransformers\DefaultPaginationTransformResult.cs" />

0 commit comments

Comments
 (0)