Skip to content

Cleaning repository of remaining business logic #579

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 13 commits into from
Oct 17, 2019
Merged
Show file tree
Hide file tree
Changes from 9 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
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
using JsonApiDotNetCore.Hooks;
using JsonApiDotNetCore.Internal;
using JsonApiDotNetCore.Internal.Contracts;
using JsonApiDotNetCore.Managers.Contracts;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Query;
using JsonApiDotNetCore.Serialization;
using JsonApiDotNetCore.Services;
using JsonApiDotNetCoreExample.Models;
using Microsoft.Extensions.Logging;
Expand All @@ -20,15 +17,14 @@ public CustomArticleService(ISortService sortService,
IFilterService filterService,
IEntityRepository<Article, int> repository,
IJsonApiOptions options,
ICurrentRequest currentRequest,
IIncludeService includeService,
ISparseFieldsService sparseFieldsService,
IPageService pageManager,
IPageService pageService,
IResourceGraph resourceGraph,
IResourceHookExecutor hookExecutor = null,
ILoggerFactory loggerFactory = null)
: base(sortService, filterService, repository, options, currentRequest, includeService, sparseFieldsService,
pageManager, resourceGraph, hookExecutor, loggerFactory)
: base(sortService, filterService, repository, options, includeService, sparseFieldsService,
pageService, resourceGraph, hookExecutor, loggerFactory)
{
}

Expand Down
137 changes: 52 additions & 85 deletions src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
using System.Linq;
using System.Threading.Tasks;
using JsonApiDotNetCore.Extensions;
using JsonApiDotNetCore.Internal;
using JsonApiDotNetCore.Internal.Contracts;
using JsonApiDotNetCore.Internal.Generics;
using JsonApiDotNetCore.Internal.Query;
using JsonApiDotNetCore.Managers.Contracts;
using JsonApiDotNetCore.Models;
using JsonApiDotNetCore.Serialization;
using Microsoft.EntityFrameworkCore;
Expand All @@ -20,52 +18,42 @@ namespace JsonApiDotNetCore.Data
/// Provides a default repository implementation and is responsible for
/// abstracting any EF Core APIs away from the service layer.
/// </summary>
public class DefaultEntityRepository<TEntity, TId>
: IEntityRepository<TEntity, TId>,
IEntityFrameworkRepository<TEntity>
public class DefaultEntityRepository<TEntity, TId> : IEntityRepository<TEntity, TId>
where TEntity : class, IIdentifiable<TId>
{
private readonly ICurrentRequest _currentRequest;
private readonly ITargetedFields _targetedFields;
private readonly DbContext _context;
private readonly DbSet<TEntity> _dbSet;
private readonly ILogger _logger;
private readonly IResourceGraph _resourceGraph;
private readonly IGenericProcessorFactory _genericProcessorFactory;
private readonly ResourceDefinition<TEntity> _resourceDefinition;

public DefaultEntityRepository(
ICurrentRequest currentRequest,
ITargetedFields updatedFields,
ITargetedFields targetedFields,
IDbContextResolver contextResolver,
IResourceGraph resourceGraph,
IGenericProcessorFactory genericProcessorFactory,
ResourceDefinition<TEntity> resourceDefinition = null)
: this(currentRequest, updatedFields, contextResolver, resourceGraph, genericProcessorFactory, resourceDefinition, null)
IGenericProcessorFactory genericProcessorFactory)
: this(targetedFields, contextResolver, resourceGraph, genericProcessorFactory, null)
{ }

public DefaultEntityRepository(
ICurrentRequest currentRequest,
ITargetedFields updatedFields,
ITargetedFields targetedFields,
IDbContextResolver contextResolver,
IResourceGraph resourceGraph,
IGenericProcessorFactory genericProcessorFactory,
ResourceDefinition<TEntity> resourceDefinition = null,
ILoggerFactory loggerFactory = null)
{
_logger = loggerFactory?.CreateLogger<DefaultEntityRepository<TEntity, TId>>();
_currentRequest = currentRequest;
_targetedFields = updatedFields;
_targetedFields = targetedFields;
_resourceGraph = resourceGraph;
_genericProcessorFactory = genericProcessorFactory;
_context = contextResolver.GetContext();
_dbSet = _context.Set<TEntity>();
_resourceDefinition = resourceDefinition;
}

/// <inheritdoc />
public virtual IQueryable<TEntity> Get() => _dbSet;

/// <inheritdoc />
public virtual IQueryable<TEntity> Get(TId id) => _dbSet.Where(e => e.Id.Equals(id));

/// <inheritdoc />
public virtual IQueryable<TEntity> Select(IQueryable<TEntity> entities, List<AttrAttribute> fields)
{
Expand All @@ -79,12 +67,9 @@ public virtual IQueryable<TEntity> Select(IQueryable<TEntity> entities, List<Att
public virtual IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQueryContext filterQueryContext)
{
if (filterQueryContext.IsCustom)
{ // todo: consider to move this business logic to service layer
var filterQuery = filterQueryContext.Query;
var defaultQueryFilters = _resourceDefinition.GetQueryFilters();
if (defaultQueryFilters != null && defaultQueryFilters.TryGetValue(filterQuery.Target, out var defaultQueryFilter) == true)
return defaultQueryFilter(entities, filterQuery);

{
var query = (Func<IQueryable<TEntity>, FilterQuery, IQueryable<TEntity>>)filterQueryContext.CustomQuery;
return query(entities, filterQueryContext.Query);
}
return entities.Filter(filterQueryContext);
}
Expand All @@ -95,21 +80,6 @@ public virtual IQueryable<TEntity> Sort(IQueryable<TEntity> entities, SortQueryC
return entities.Sort(sortQueryContext);
}

/// <inheritdoc />
public virtual async Task<TEntity> GetAsync(TId id, List<AttrAttribute> fields = null)
{
return await Select(Get(), fields).SingleOrDefaultAsync(e => e.Id.Equals(id));
}

/// <inheritdoc />
public virtual async Task<TEntity> GetAndIncludeAsync(TId id, RelationshipAttribute relationship, List<AttrAttribute> fields = null)
{
_logger?.LogDebug($"[JADN] GetAndIncludeAsync({id}, {relationship.PublicRelationshipName})");
var includedSet = Include(Select(Get(), fields), relationship);
var result = await includedSet.SingleOrDefaultAsync(e => e.Id.Equals(id));
return result;
}

/// <inheritdoc />
public virtual async Task<TEntity> CreateAsync(TEntity entity)
{
Expand All @@ -134,6 +104,10 @@ public virtual async Task<TEntity> CreateAsync(TEntity entity)
_dbSet.Add(entity);
await _context.SaveChangesAsync();

// this ensures relationships get reloaded from the database if they have
// been requested. See https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/343
DetachRelationships(entity);

return entity;
}

Expand Down Expand Up @@ -169,7 +143,7 @@ private void LoadInverseRelationships(object trackedRelationshipValue, Relations

private bool IsHasOneRelationship(string internalRelationshipName, Type type)
{
var relationshipAttr = _resourceGraph.GetContextEntity(type).Relationships.SingleOrDefault(r => r.InternalRelationshipName == internalRelationshipName);
var relationshipAttr = _resourceGraph.GetContextEntity(type).Relationships.FirstOrDefault(r => r.InternalRelationshipName == internalRelationshipName);
if (relationshipAttr != null)
{
if (relationshipAttr is HasOneAttribute)
Expand All @@ -182,9 +156,7 @@ private bool IsHasOneRelationship(string internalRelationshipName, Type type)
return !(type.GetProperty(internalRelationshipName).PropertyType.Inherits(typeof(IEnumerable)));
}


/// <inheritdoc />
public void DetachRelationshipPointers(TEntity entity)
private void DetachRelationships(TEntity entity)
{
foreach (var relationshipAttr in _targetedFields.Relationships)
{
Expand Down Expand Up @@ -212,19 +184,19 @@ public void DetachRelationshipPointers(TEntity entity)
/// <inheritdoc />
public virtual async Task<TEntity> UpdateAsync(TEntity updatedEntity)
{
var databaseEntity = await GetAsync(updatedEntity.Id);
var databaseEntity = await Get(updatedEntity.Id).FirstOrDefaultAsync();
if (databaseEntity == null)
return null;

foreach (var attr in _targetedFields.Attributes)
attr.SetValue(databaseEntity, attr.GetValue(updatedEntity));
foreach (var attribute in _targetedFields.Attributes)
attribute.SetValue(databaseEntity, attribute.GetValue(updatedEntity));

foreach (var relationshipAttr in _targetedFields.Relationships)
{
/// loads databasePerson.todoItems
LoadCurrentRelationships(databaseEntity, relationshipAttr);
/// trackedRelationshipValue is either equal to updatedPerson.todoItems
/// or replaced with the same set of todoItems from the EF Core change tracker,
/// or replaced with the same set of todoItems from the EF Core change tracker,
/// if they were already tracked
object trackedRelationshipValue = GetTrackedRelationshipValue(relationshipAttr, updatedEntity, out bool wasAlreadyTracked);
/// loads into the db context any persons currentlresy related
Expand All @@ -238,7 +210,6 @@ public virtual async Task<TEntity> UpdateAsync(TEntity updatedEntity)
return databaseEntity;
}


/// <summary>
/// Responsible for getting the relationship value for a given relationship
/// attribute of a given entity. It ensures that the relationship value
Expand Down Expand Up @@ -302,11 +273,10 @@ public async Task UpdateRelationshipsAsync(object parent, RelationshipAttribute
await genericProcessor.UpdateRelationshipsAsync(parent, relationship, relationshipIds);
}


/// <inheritdoc />
public virtual async Task<bool> DeleteAsync(TId id)
{
var entity = await GetAsync(id);
var entity = await Get(id).FirstOrDefaultAsync();
if (entity == null) return false;
_dbSet.Remove(entity);
await _context.SaveChangesAsync();
Expand All @@ -327,33 +297,6 @@ public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, params
return entities.Include(internalRelationshipPath);
}

/// <inheritdoc />
public virtual IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relationshipName)
{
if (string.IsNullOrWhiteSpace(relationshipName)) throw new JsonApiException(400, "Include parameter must not be empty if provided");

var relationshipChain = relationshipName.Split('.');

// variables mutated in recursive loop
// TODO: make recursive method
string internalRelationshipPath = null;
var entity = _currentRequest.GetRequestResource();
for (var i = 0; i < relationshipChain.Length; i++)
{
var requestedRelationship = relationshipChain[i];
var relationship = entity.Relationships.FirstOrDefault(r => r.PublicRelationshipName == requestedRelationship);

internalRelationshipPath = (internalRelationshipPath == null)
? relationship.RelationshipPath
: $"{internalRelationshipPath}.{relationship.RelationshipPath}";

if (i < relationshipChain.Length)
entity = _resourceGraph.GetContextEntity(relationship.Type);
}

return entities.Include(internalRelationshipPath);
}

/// <inheritdoc />
public virtual async Task<IEnumerable<TEntity>> PageAsync(IQueryable<TEntity> entities, int pageSize, int pageNumber)
{
Expand Down Expand Up @@ -490,19 +433,43 @@ private IIdentifiable AttachOrGetTracked(IIdentifiable relationshipValue)
_context.Entry(relationshipValue).State = EntityState.Unchanged;
return null;
}

[Obsolete("Use method Select(IQueryable<TEntity>, List<AttrAttribute>) instead. See @MIGRATION_LINK for details.", true)]
public IQueryable<TEntity> Select(IQueryable<TEntity> entities, List<string> fields) => throw new NotImplementedException();
[Obsolete("Use method Include(IQueryable<TEntity>, params RelationshipAttribute[]) instead. See @MIGRATION_LINK for details.", true)]
public IQueryable<TEntity> Include(IQueryable<TEntity> entities, string relationshipName) => throw new NotImplementedException();
[Obsolete("Use method Filter(IQueryable<TEntity>, FilterQueryContext) instead. See @MIGRATION_LINK for details.", true)]
public IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQuery filterQuery) => throw new NotImplementedException();
[Obsolete("Use method Sort(IQueryable<TEntity>, SortQueryContext) instead. See @MIGRATION_LINK for details.", true)]
public IQueryable<TEntity> Sort(IQueryable<TEntity> entities, List<SortQuery> sortQueries) => throw new NotImplementedException();
[Obsolete("Use method Get(TId id) and FirstOrDefaultAsync(IQueryable<TEntity>) separatedly instead. See @MIGRATION_LINK for details.", true)]
public Task<TEntity> GetAsync(TId id) => throw new NotImplementedException();
[Obsolete("Use methods Get(TId id) and Include(IQueryable<TEntity>, params RelationshipAttribute[]) separatedly instead. See @MIGRATION_LINK for details.", true)]
public Task<TEntity> GetAndIncludeAsync(TId id, string relationshipName) => throw new NotImplementedException();
[Obsolete("Use method UpdateAsync(TEntity) instead. See @MIGRATION_LINK for details.")]
public Task<TEntity> UpdateAsync(TId id, TEntity entity) => throw new NotImplementedException();
[Obsolete("This has been removed, see @TODO_MIGRATION_LINK for details", true)]
public void DetachRelationshipPointers(TEntity entity) { }
}

/// <inheritdoc />
public class DefaultEntityRepository<TEntity>
: DefaultEntityRepository<TEntity, int>,
IEntityRepository<TEntity>
public class DefaultEntityRepository<TEntity> : DefaultEntityRepository<TEntity, int>, IEntityRepository<TEntity>
where TEntity : class, IIdentifiable<int>
{
public DefaultEntityRepository(ICurrentRequest currentRequest, ITargetedFields updatedFields, IDbContextResolver contextResolver, IResourceGraph resourceGraph, IGenericProcessorFactory genericProcessorFactory, ResourceDefinition<TEntity> resourceDefinition = null) : base(currentRequest, updatedFields, contextResolver, resourceGraph, genericProcessorFactory, resourceDefinition)
public DefaultEntityRepository(ITargetedFields targetedFields,
IDbContextResolver contextResolver,
IResourceGraph resourceGraph,
IGenericProcessorFactory genericProcessorFactory)
: base(targetedFields, contextResolver, resourceGraph, genericProcessorFactory)
{
}

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)
public DefaultEntityRepository(ITargetedFields targetedFields,
IDbContextResolver contextResolver,
IResourceGraph resourceGraph,
IGenericProcessorFactory genericProcessorFactory,
ILoggerFactory loggerFactory = null)
: base(targetedFields, contextResolver, resourceGraph, genericProcessorFactory, loggerFactory)
{
}
}
Expand Down
Loading