Skip to content

Commit 4c067db

Browse files
authored
Cleaning repository of remaining business logic (#579)
* feat: first draft of attribute diffing in repo layer * chore: remove EF Core-specific DetachRelationshipPointers from repository interface * chore: replace SingleOrDefaultAsync with FirstOrDefaultAsync for increased performance * chore: decoupled ResourceDefinition from repository layer in Filter implementation * chore: remove updatevaluehelper * fix: Can_Patch_Entity test * fix: EntityResourceTest, variable name updatedFields -> targetedFields * chore: add comment referencing to github issue * chore: rename pageManager -> pageService * chore: remove deprecations * fix: undid wrong replace all of BeforeUpdate -> pageService * chore: remove unreferenced method in ResponseSerializer
1 parent ee7b2c8 commit 4c067db

26 files changed

+192
-274
lines changed

src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs

+3-7
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
using JsonApiDotNetCore.Hooks;
44
using JsonApiDotNetCore.Internal;
55
using JsonApiDotNetCore.Internal.Contracts;
6-
using JsonApiDotNetCore.Managers.Contracts;
7-
using JsonApiDotNetCore.Models;
86
using JsonApiDotNetCore.Query;
9-
using JsonApiDotNetCore.Serialization;
107
using JsonApiDotNetCore.Services;
118
using JsonApiDotNetCoreExample.Models;
129
using Microsoft.Extensions.Logging;
@@ -20,15 +17,14 @@ public CustomArticleService(ISortService sortService,
2017
IFilterService filterService,
2118
IEntityRepository<Article, int> repository,
2219
IJsonApiOptions options,
23-
ICurrentRequest currentRequest,
2420
IIncludeService includeService,
2521
ISparseFieldsService sparseFieldsService,
26-
IPageService pageManager,
22+
IPageService pageService,
2723
IResourceGraph resourceGraph,
2824
IResourceHookExecutor hookExecutor = null,
2925
ILoggerFactory loggerFactory = null)
30-
: base(sortService, filterService, repository, options, currentRequest, includeService, sparseFieldsService,
31-
pageManager, resourceGraph, hookExecutor, loggerFactory)
26+
: base(sortService, filterService, repository, options, includeService, sparseFieldsService,
27+
pageService, resourceGraph, hookExecutor, loggerFactory)
3228
{
3329
}
3430

src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

+40-94
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@
44
using System.Linq;
55
using System.Threading.Tasks;
66
using JsonApiDotNetCore.Extensions;
7-
using JsonApiDotNetCore.Internal;
87
using JsonApiDotNetCore.Internal.Contracts;
98
using JsonApiDotNetCore.Internal.Generics;
109
using JsonApiDotNetCore.Internal.Query;
11-
using JsonApiDotNetCore.Managers.Contracts;
1210
using JsonApiDotNetCore.Models;
1311
using JsonApiDotNetCore.Serialization;
1412
using Microsoft.EntityFrameworkCore;
@@ -20,52 +18,42 @@ namespace JsonApiDotNetCore.Data
2018
/// Provides a default repository implementation and is responsible for
2119
/// abstracting any EF Core APIs away from the service layer.
2220
/// </summary>
23-
public class DefaultEntityRepository<TEntity, TId>
24-
: IEntityRepository<TEntity, TId>,
25-
IEntityFrameworkRepository<TEntity>
21+
public class DefaultEntityRepository<TEntity, TId> : IEntityRepository<TEntity, TId>
2622
where TEntity : class, IIdentifiable<TId>
2723
{
28-
private readonly ICurrentRequest _currentRequest;
2924
private readonly ITargetedFields _targetedFields;
3025
private readonly DbContext _context;
3126
private readonly DbSet<TEntity> _dbSet;
32-
private readonly ILogger _logger;
3327
private readonly IResourceGraph _resourceGraph;
3428
private readonly IGenericProcessorFactory _genericProcessorFactory;
35-
private readonly ResourceDefinition<TEntity> _resourceDefinition;
3629

3730
public DefaultEntityRepository(
38-
ICurrentRequest currentRequest,
39-
ITargetedFields updatedFields,
31+
ITargetedFields targetedFields,
4032
IDbContextResolver contextResolver,
4133
IResourceGraph resourceGraph,
42-
IGenericProcessorFactory genericProcessorFactory,
43-
ResourceDefinition<TEntity> resourceDefinition = null)
44-
: this(currentRequest, updatedFields, contextResolver, resourceGraph, genericProcessorFactory, resourceDefinition, null)
34+
IGenericProcessorFactory genericProcessorFactory)
35+
: this(targetedFields, contextResolver, resourceGraph, genericProcessorFactory, null)
4536
{ }
4637

4738
public DefaultEntityRepository(
48-
ICurrentRequest currentRequest,
49-
ITargetedFields updatedFields,
39+
ITargetedFields targetedFields,
5040
IDbContextResolver contextResolver,
5141
IResourceGraph resourceGraph,
5242
IGenericProcessorFactory genericProcessorFactory,
53-
ResourceDefinition<TEntity> resourceDefinition = null,
5443
ILoggerFactory loggerFactory = null)
5544
{
56-
_logger = loggerFactory?.CreateLogger<DefaultEntityRepository<TEntity, TId>>();
57-
_currentRequest = currentRequest;
58-
_targetedFields = updatedFields;
45+
_targetedFields = targetedFields;
5946
_resourceGraph = resourceGraph;
6047
_genericProcessorFactory = genericProcessorFactory;
6148
_context = contextResolver.GetContext();
6249
_dbSet = _context.Set<TEntity>();
63-
_resourceDefinition = resourceDefinition;
6450
}
6551

6652
/// <inheritdoc />
6753
public virtual IQueryable<TEntity> Get() => _dbSet;
68-
54+
/// <inheritdoc />
55+
public virtual IQueryable<TEntity> Get(TId id) => _dbSet.Where(e => e.Id.Equals(id));
56+
6957
/// <inheritdoc />
7058
public virtual IQueryable<TEntity> Select(IQueryable<TEntity> entities, List<AttrAttribute> fields)
7159
{
@@ -79,12 +67,9 @@ public virtual IQueryable<TEntity> Select(IQueryable<TEntity> entities, List<Att
7967
public virtual IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQueryContext filterQueryContext)
8068
{
8169
if (filterQueryContext.IsCustom)
82-
{ // todo: consider to move this business logic to service layer
83-
var filterQuery = filterQueryContext.Query;
84-
var defaultQueryFilters = _resourceDefinition.GetQueryFilters();
85-
if (defaultQueryFilters != null && defaultQueryFilters.TryGetValue(filterQuery.Target, out var defaultQueryFilter) == true)
86-
return defaultQueryFilter(entities, filterQuery);
87-
70+
{
71+
var query = (Func<IQueryable<TEntity>, FilterQuery, IQueryable<TEntity>>)filterQueryContext.CustomQuery;
72+
return query(entities, filterQueryContext.Query);
8873
}
8974
return entities.Filter(filterQueryContext);
9075
}
@@ -95,45 +80,30 @@ public virtual IQueryable<TEntity> Sort(IQueryable<TEntity> entities, SortQueryC
9580
return entities.Sort(sortQueryContext);
9681
}
9782

98-
/// <inheritdoc />
99-
public virtual async Task<TEntity> GetAsync(TId id, List<AttrAttribute> fields = null)
100-
{
101-
return await Select(Get(), fields).SingleOrDefaultAsync(e => e.Id.Equals(id));
102-
}
103-
104-
/// <inheritdoc />
105-
public virtual async Task<TEntity> GetAndIncludeAsync(TId id, RelationshipAttribute relationship, List<AttrAttribute> fields = null)
106-
{
107-
_logger?.LogDebug($"[JADN] GetAndIncludeAsync({id}, {relationship.PublicRelationshipName})");
108-
var includedSet = Include(Select(Get(), fields), relationship);
109-
var result = await includedSet.SingleOrDefaultAsync(e => e.Id.Equals(id));
110-
return result;
111-
}
112-
11383
/// <inheritdoc />
11484
public virtual async Task<TEntity> CreateAsync(TEntity entity)
11585
{
11686
foreach (var relationshipAttr in _targetedFields.Relationships)
11787
{
118-
var trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, entity, out bool wasAlreadyTracked);
88+
object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, entity, out bool wasAlreadyTracked);
11989
LoadInverseRelationships(trackedRelationshipValue, relationshipAttr);
12090
if (wasAlreadyTracked)
121-
{
12291
/// We only need to reassign the relationship value to the to-be-added
12392
/// entity when we're using a different instance (because this different one
12493
/// was already tracked) than the one assigned to the to-be-created entity.
12594
AssignRelationshipValue(entity, trackedRelationshipValue, relationshipAttr);
126-
}
12795
else if (relationshipAttr is HasManyThroughAttribute throughAttr)
128-
{
12996
/// even if we don't have to reassign anything because of already tracked
13097
/// entities, we still need to assign the "through" entities in the case of many-to-many.
13198
AssignHasManyThrough(entity, throughAttr, (IList)trackedRelationshipValue);
132-
}
13399
}
134100
_dbSet.Add(entity);
135101
await _context.SaveChangesAsync();
136102

103+
// this ensures relationships get reloaded from the database if they have
104+
// been requested. See https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/343
105+
DetachRelationships(entity);
106+
137107
return entity;
138108
}
139109

@@ -169,7 +139,7 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations
169139

170140
private bool IsHasOneRelationship(string internalRelationshipName, Type type)
171141
{
172-
var relationshipAttr = _resourceGraph.GetContextEntity(type).Relationships.SingleOrDefault(r => r.InternalRelationshipName == internalRelationshipName);
142+
var relationshipAttr = _resourceGraph.GetContextEntity(type).Relationships.FirstOrDefault(r => r.InternalRelationshipName == internalRelationshipName);
173143
if (relationshipAttr != null)
174144
{
175145
if (relationshipAttr is HasOneAttribute)
@@ -182,9 +152,7 @@ private bool IsHasOneRelationship(string internalRelationshipName, Type type)
182152
return !(type.GetProperty(internalRelationshipName).PropertyType.Inherits(typeof(IEnumerable)));
183153
}
184154

185-
186-
/// <inheritdoc />
187-
public void DetachRelationshipPointers(TEntity entity)
155+
private void DetachRelationships(TEntity entity)
188156
{
189157
foreach (var relationshipAttr in _targetedFields.Relationships)
190158
{
@@ -212,22 +180,22 @@ public void DetachRelationshipPointers(TEntity entity)
212180
/// <inheritdoc />
213181
public virtual async Task<TEntity> UpdateAsync(TEntity updatedEntity)
214182
{
215-
var databaseEntity = await GetAsync(updatedEntity.Id);
183+
var databaseEntity = await Get(updatedEntity.Id).FirstOrDefaultAsync();
216184
if (databaseEntity == null)
217185
return null;
218186

219-
foreach (var attr in _targetedFields.Attributes)
220-
attr.SetValue(databaseEntity, attr.GetValue(updatedEntity));
187+
foreach (var attribute in _targetedFields.Attributes)
188+
attribute.SetValue(databaseEntity, attribute.GetValue(updatedEntity));
221189

222190
foreach (var relationshipAttr in _targetedFields.Relationships)
223191
{
224192
/// loads databasePerson.todoItems
225193
LoadCurrentRelationships(databaseEntity, relationshipAttr);
226-
/// trackedRelationshipValue is either equal to updatedPerson.todoItems
227-
/// or replaced with the same set of todoItems from the EF Core change tracker,
228-
/// if they were already tracked
229-
object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, updatedEntity, out bool wasAlreadyTracked);
230-
/// loads into the db context any persons currentlresy related
194+
/// trackedRelationshipValue is either equal to updatedPerson.todoItems,
195+
/// or replaced with the same set (same ids) of todoItems from the EF Core change tracker,
196+
/// which is the case if they were already tracked
197+
object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, updatedEntity, out _);
198+
/// loads into the db context any persons currently related
231199
/// to the todoItems in trackedRelationshipValue
232200
LoadInverseRelationships(trackedRelationshipValue, relationshipAttr);
233201
/// assigns the updated relationship to the database entity
@@ -238,7 +206,6 @@ public virtual async Task<TEntity> UpdateAsync(TEntity updatedEntity)
238206
return databaseEntity;
239207
}
240208

241-
242209
/// <summary>
243210
/// Responsible for getting the relationship value for a given relationship
244211
/// attribute of a given entity. It ensures that the relationship value
@@ -302,11 +269,10 @@ public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute
302269
await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds);
303270
}
304271

305-
306272
/// <inheritdoc />
307273
public virtual async Task<bool> DeleteAsync(TId id)
308274
{
309-
var entity = await GetAsync(id);
275+
var entity = await Get(id).FirstOrDefaultAsync();
310276
if (entity == null) return false;
311277
_dbSet.Remove(entity);
312278
await _context.SaveChangesAsync();
@@ -327,33 +293,6 @@ public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, params
327293
return entities.Include(internalRelationshipPath);
328294
}
329295

330-
/// <inheritdoc />
331-
public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relationshipName)
332-
{
333-
if (string.IsNullOrWhiteSpace(relationshipName)) throw new JsonApiException(400, "Include parameter must not be empty if provided");
334-
335-
var relationshipChain = relationshipName.Split('.');
336-
337-
// variables mutated in recursive loop
338-
// TODO: make recursive method
339-
string internalRelationshipPath = null;
340-
var entity = _currentRequest.GetRequestResource();
341-
for (var i = 0; i < relationshipChain.Length; i++)
342-
{
343-
var requestedRelationship = relationshipChain[i];
344-
var relationship = entity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == requestedRelationship);
345-
346-
internalRelationshipPath = (internalRelationshipPath == null)
347-
? relationship.RelationshipPath
348-
: $"{internalRelationshipPath}.{relationship.RelationshipPath}";
349-
350-
if (i < relationshipChain.Length)
351-
entity = _resourceGraph.GetContextEntity(relationship.Type);
352-
}
353-
354-
return entities.Include(internalRelationshipPath);
355-
}
356-
357296
/// <inheritdoc />
358297
public virtual async Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> entities, int pageSize, int pageNumber)
359298
{
@@ -493,16 +432,23 @@ private IIdentifiable AttachOrGetTracked(IIdentifiable relationshipValue)
493432
}
494433

495434
/// <inheritdoc />
496-
public class DefaultEntityRepository<TEntity>
497-
: DefaultEntityRepository<TEntity, int>,
498-
IEntityRepository<TEntity>
435+
public class DefaultEntityRepository<TEntity> : DefaultEntityRepository<TEntity, int>, IEntityRepository<TEntity>
499436
where TEntity : class, IIdentifiable<int>
500437
{
501-
public DefaultEntityRepository(ICurrentRequest currentRequest, ITargetedFields updatedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, IGenericProcessorFactory genericProcessorFactory, ResourceDefinition<TEntity> resourceDefinition = null) : base(currentRequest, updatedFields, contextResolver, resourceGraph, genericProcessorFactory, resourceDefinition)
438+
public DefaultEntityRepository(ITargetedFields targetedFields,
439+
IDbContextResolver contextResolver,
440+
IResourceGraph resourceGraph,
441+
IGenericProcessorFactory genericProcessorFactory)
442+
: base(targetedFields, contextResolver, resourceGraph, genericProcessorFactory)
502443
{
503444
}
504445

505-
public DefaultEntityRepository(ICurrentRequest currentRequest, ITargetedFields updatedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, IGenericProcessorFactory genericProcessorFactory, ResourceDefinition<TEntity> resourceDefinition = null, ILoggerFactory loggerFactory = null) : base(currentRequest, updatedFields, contextResolver, resourceGraph, genericProcessorFactory, resourceDefinition, loggerFactory)
446+
public DefaultEntityRepository(ITargetedFields targetedFields,
447+
IDbContextResolver contextResolver,
448+
IResourceGraph resourceGraph,
449+
IGenericProcessorFactory genericProcessorFactory,
450+
ILoggerFactory loggerFactory = null)
451+
: base(targetedFields, contextResolver, resourceGraph, genericProcessorFactory, loggerFactory)
506452
{
507453
}
508454
}

src/JsonApiDotNetCore/Data/IEntityReadRepository.cs

+4-27
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ public interface IEntityReadRepository<TEntity, in TId>
2020
/// such as authorization of resources.
2121
/// </summary>
2222
IQueryable<TEntity> Get();
23-
23+
/// <summary>
24+
/// Get the entity by id
25+
/// </summary>
26+
IQueryable<TEntity> Get(TId id);
2427
/// <summary>
2528
/// Apply fields to the provided queryable
2629
/// </summary>
2730
IQueryable<TEntity> Select(IQueryable<TEntity> entities, List<AttrAttribute> fields);
28-
2931
/// <summary>
3032
/// Include a relationship in the query
3133
/// </summary>
@@ -35,51 +37,26 @@ public interface IEntityReadRepository<TEntity, in TId>
3537
/// </code>
3638
/// </example>
3739
IQueryable<TEntity> Include(IQueryable<TEntity> entities, params RelationshipAttribute[] inclusionChain);
38-
[Obsolete]
39-
IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relationshipName);
40-
4140
/// <summary>
4241
/// Apply a filter to the provided queryable
4342
/// </summary>
4443
IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQueryContext filterQuery);
45-
4644
/// <summary>
4745
/// Apply a sort to the provided queryable
4846
/// </summary>
4947
IQueryable<TEntity> Sort(IQueryable<TEntity> entities, SortQueryContext sortQueries);
50-
5148
/// <summary>
5249
/// Paginate the provided queryable
5350
/// </summary>
5451
Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> entities, int pageSize, int pageNumber);
55-
56-
/// <summary>
57-
/// Get the entity by id
58-
/// </summary>
59-
Task<TEntity> GetAsync(TId id, List<AttrAttribute> fields = null);
60-
61-
/// <summary>
62-
/// Get the entity with the specified id and include the relationship.
63-
/// </summary>
64-
/// <param name="id">The entity id</param>
65-
/// <param name="relationship">The exposed relationship</param>
66-
/// <example>
67-
/// <code>
68-
/// _todoItemsRepository.GetAndIncludeAsync(1, "achieved-date");
69-
/// </code>
70-
/// </example>
71-
Task<TEntity> GetAndIncludeAsync(TId id, RelationshipAttribute relationship, List<AttrAttribute> fields = null);
72-
7352
/// <summary>
7453
/// Count the total number of records
7554
/// </summary>
7655
Task<int> CountAsync(IQueryable<TEntity> entities);
77-
7856
/// <summary>
7957
/// Get the first element in the collection, return the default value if collection is empty
8058
/// </summary>
8159
Task<TEntity> FirstOrDefaultAsync(IQueryable<TEntity> entities);
82-
8360
/// <summary>
8461
/// Convert the collection to a materialized list
8562
/// </summary>

0 commit comments

Comments
 (0)