Skip to content
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

Add SoftDeleted repository and change Directions #1201

Merged
merged 9 commits into from
Aug 28, 2023
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 OutOfSchool/OutOfSchool.DataAccess/Models/Direction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace OutOfSchool.Services.Models;

public class Direction : IKeyedEntity<long>
public class Direction : IKeyedEntity<long>, ISoftDeleted
{
public long Id { get; set; }

Expand Down
6 changes: 6 additions & 0 deletions OutOfSchool/OutOfSchool.DataAccess/Models/ISoftDeleted.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace OutOfSchool.Services.Models;

public interface ISoftDeleted
{
bool IsDeleted { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
Expand Down Expand Up @@ -126,10 +127,11 @@ public virtual async Task<IEnumerable<TEntity>> GetAllWithDetails(string include
return await query.ToListAsync();
}

public virtual async Task<IEnumerable<TEntity>> GetByFilter(Expression<Func<TEntity, bool>> predicate,
public virtual async Task<IEnumerable<TEntity>> GetByFilter(
Expression<Func<TEntity, bool>> whereExpression,
string includeProperties = "")
{
var query = this.dbSet.Where(predicate);
var query = this.dbSet.Where(whereExpression);

foreach (var includeProperty in includeProperties.Split(
new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
Expand All @@ -141,10 +143,11 @@ public virtual async Task<IEnumerable<TEntity>> GetByFilter(Expression<Func<TEnt
}

/// <inheritdoc/>
public virtual IQueryable<TEntity> GetByFilterNoTracking(Expression<Func<TEntity, bool>> predicate,
public virtual IQueryable<TEntity> GetByFilterNoTracking(
Expression<Func<TEntity, bool>> whereExpression,
string includeProperties = "")
{
var query = this.dbSet.Where(predicate);
var query = this.dbSet.Where(whereExpression);

foreach (var includeProperty in includeProperties.Split(
new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
Expand All @@ -169,33 +172,34 @@ public virtual async Task<TEntity> Update(TEntity entity)
}

/// <inheritdoc/>
public virtual Task<int> Count(Expression<Func<TEntity, bool>> where = null)
public virtual Task<int> Count(Expression<Func<TEntity, bool>> whereExpression = null)
{
return where == null
return whereExpression == null
? dbSet.CountAsync()
: dbSet.Where(where).CountAsync();
: dbSet.Where(whereExpression).CountAsync();
}

/// <inheritdoc/>
public virtual Task<bool> Any(Expression<Func<TEntity, bool>> where = null)
public virtual Task<bool> Any(Expression<Func<TEntity, bool>> whereExpression = null)
{
return where == null
return whereExpression == null
? dbSet.AnyAsync()
: dbSet.Where(where).AnyAsync();
: dbSet.Where(whereExpression).AnyAsync();
}

/// <inheritdoc/>
public virtual IQueryable<TEntity> Get(int skip = 0,
public virtual IQueryable<TEntity> Get(
int skip = 0,
int take = 0,
string includeProperties = "",
Expression<Func<TEntity, bool>> where = null,
Expression<Func<TEntity, bool>> whereExpression = null,
Dictionary<Expression<Func<TEntity, object>>, SortDirection> orderBy = null,
bool asNoTracking = false)
{
IQueryable<TEntity> query = dbSet;
if (where != null)
if (whereExpression != null)
{
query = query.Where(where);
query = query.Where(whereExpression);
}

if ((orderBy != null) && orderBy.Any())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Security.AccessControl;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using OutOfSchool.Services.Enums;
using OutOfSchool.Services.Extensions;
using OutOfSchool.Services.Models;

namespace OutOfSchool.Services.Repository;

public class EntityRepositorySoftDeleted<TKey, TEntity> : EntityRepositoryBase<TKey, TEntity>, IEntityRepositorySoftDeleted<TKey, TEntity>
where TEntity : class, IKeyedEntity<TKey>, ISoftDeleted, new()
where TKey : IEquatable<TKey>
{
/// <summary>
/// Initializes a new instance of the <see cref="EntityRepositorySoftDeleted{TKey, TEntity}"/> class.
/// </summary>
/// <param name="dbContext">OutOfSchoolDbContext.</param>
public EntityRepositorySoftDeleted(OutOfSchoolDbContext dbContext)
: base(dbContext)
{
}

/// <inheritdoc/>
public override async Task<IEnumerable<TEntity>> GetAll()
{
return await dbSet.Where(x => !x.IsDeleted).ToListAsync().ConfigureAwait(false);
}

/// <inheritdoc/>
public override async Task<IEnumerable<TEntity>> GetAllWithDetails(string includeProperties = "")
{
IQueryable<TEntity> query = dbSet.Where(x => !x.IsDeleted);

foreach (var includeProperty in includeProperties.Split(
new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}

return await query.ToListAsync().ConfigureAwait(false);
}

/// <inheritdoc/>
public override async Task<IEnumerable<TEntity>> GetByFilter(
Expression<Func<TEntity, bool>> whereExpression,
string includeProperties = "")
{
whereExpression = GetWhereExpression(whereExpression);
return await base.GetByFilter(whereExpression, includeProperties).ConfigureAwait(false);
}

/// <inheritdoc/>
public override IQueryable<TEntity> GetByFilterNoTracking(
Expression<Func<TEntity, bool>> whereExpression,
string includeProperties = "")
{
whereExpression = GetWhereExpression(whereExpression);
return base.GetByFilterNoTracking(whereExpression, includeProperties);
}

/// <inheritdoc/>
public override Task<TEntity> GetById(TKey id) => dbSet.FirstOrDefaultAsync(x => !x.IsDeleted && x.Id.Equals(id));

/// <inheritdoc/>
public override Task<int> Count(Expression<Func<TEntity, bool>> whereExpression = null)
{
return base.Count(GetWhereExpression(whereExpression));
}

public override Task<bool> Any(Expression<Func<TEntity, bool>> whereExpression = null)
{
return base.Any(GetWhereExpression(whereExpression));
}

public override IQueryable<TEntity> Get(
int skip = 0,
int take = 0,
string includeProperties = "",
Expression<Func<TEntity, bool>> whereExpression = null,
Dictionary<Expression<Func<TEntity, object>>, SortDirection> orderBy = null,
bool asNoTracking = false)
{
return base.Get(skip, take, includeProperties, GetWhereExpression(whereExpression), orderBy, asNoTracking);
}

private Expression<Func<TEntity, bool>> GetWhereExpression(Expression<Func<TEntity, bool>> whereExpression)
{
if (whereExpression != null)
{
Expression<Func<TEntity, bool>> right = x => !x.IsDeleted;
whereExpression = Expression.Lambda<Func<TEntity, bool>>(
Expression.AndAlso(
whereExpression.Body,
new ExpressionParameterReplacer(right.Parameters, whereExpression.Parameters).Visit(right.Body)
),
whereExpression.Parameters);
}
else
{
whereExpression = x => !x.IsDeleted;
}

return whereExpression;
}

private sealed class ExpressionParameterReplacer : ExpressionVisitor
{
private IDictionary<ParameterExpression, ParameterExpression> ParameterReplacements { get; set; }

public ExpressionParameterReplacer(
IList<ParameterExpression> fromParameters,
IList<ParameterExpression> toParameters)
{
ParameterReplacements = fromParameters.Zip(toParameters, (k, v) => new { k, v })
.ToDictionary(x => x.k, x => x.v);
}

protected override Expression VisitParameter(ParameterExpression node)
{
ParameterExpression replacement;

if (ParameterReplacements.TryGetValue(node, out replacement))
{ node = replacement; }

return base.VisitParameter(node);
}
}
}
34 changes: 23 additions & 11 deletions OutOfSchool/OutOfSchool.DataAccess/Repository/IEntityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,53 +95,54 @@ public interface IEntityRepositoryBase<TKey, TEntity>
/// <summary>
/// Get elements by a specific filter.
/// </summary>
/// <param name="predicate">Filter with key.</param>
/// <param name="whereExpression">Filter with key.</param>
/// <param name="includeProperties">Name of properties which should be included.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.
/// The task result contains a <see cref="IEnumerable{T}"/> that contains elements.</returns>
Task<IEnumerable<TEntity>> GetByFilter(Expression<Func<TEntity, bool>> predicate, string includeProperties = "");
Task<IEnumerable<TEntity>> GetByFilter(Expression<Func<TEntity, bool>> whereExpression, string includeProperties = "");

/// <summary>
/// Get elements by a specific filter with no tracking.
/// </summary>
/// <param name="predicate">Filter with key.</param>
/// <param name="whereExpression">Filter with key.</param>
/// <param name="includeProperties">Name of properties which should be included.</param>
/// <returns>An <see cref="IQueryable{TResult}"/> that contains elements from the input sequence that
/// satisfy the condition specified by predicate.
IQueryable<TEntity> GetByFilterNoTracking(Expression<Func<TEntity, bool>> predicate, string includeProperties = "");
IQueryable<TEntity> GetByFilterNoTracking(Expression<Func<TEntity, bool>> whereExpression, string includeProperties = "");

/// <summary>
/// Get the amount of elements with filter or without it.
/// </summary>
/// <param name="where">Filter.</param>
/// <param name="whereExpression">Filter.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.
/// The task result contains an amount of found elements.</returns>
Task<int> Count(Expression<Func<TEntity, bool>> where = null);
Task<int> Count(Expression<Func<TEntity, bool>> whereExpression = null);

/// <summary>
/// Asynchronously determines whether any element of a sequence satisfies a condition.
/// </summary>
/// <param name="where">Filter.</param>
/// <param name="whereExpression">Filter.</param>
/// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.
/// The task result contains <see langword="true" /> if any elements in the source sequence pass the test in the specified
/// filter; otherwise, <see langword="false" />.</returns>
Task<bool> Any(Expression<Func<TEntity, bool>> where = null);
Task<bool> Any(Expression<Func<TEntity, bool>> whereExpression = null);

/// <summary>
/// Get ordered, filtered list of elements.
/// </summary>
/// <param name="skip">How many records we want tp skip.</param>
/// <param name="take">How many records we want to take.</param>
/// <param name="includeProperties">What Properties we want to include to objects that we will receive.</param>
/// <param name="where">Filter.</param>
/// <param name="whereExpression">Filter.</param>
/// <param name="orderBy">Filter that defines by wich properties we want to order by with ascending or descending ordering.</param>
/// <param name="asNoTracking">Define if the result set will be tracked by the context.</param>
/// <returns>An <see cref="IQueryable{TResult}"/> that contains elements from the input sequence that
/// satisfy the condition specified by predicate. An ordered, filtered <see cref="IQueryable{T}"/>.</returns>
IQueryable<TEntity> Get(int skip = 0,
IQueryable<TEntity> Get(
int skip = 0,
int take = 0,
string includeProperties = "",
Expression<Func<TEntity, bool>> where = null,
Expression<Func<TEntity, bool>> whereExpression = null,
Dictionary<Expression<Func<TEntity, object>>, SortDirection> orderBy = null,
bool asNoTracking = false);
}
Expand All @@ -160,4 +161,15 @@ public interface IEntityRepository<TKey, TEntity> : IEntityRepositoryBase<TKey,
public interface ISensitiveEntityRepository<TEntity> : IEntityRepositoryBase<Guid, TEntity>
where TEntity : class, IKeyedEntity<Guid>, new()
{
}

public interface IEntityRepositorySoftDeleted<TKey, TEntity> : IEntityRepositoryBase<TKey, TEntity>
where TEntity : class, IKeyedEntity<TKey>, ISoftDeleted, new()
where TKey : IEquatable<TKey>
{
}

public interface ISensitiveEntityRepositorySoftDeleted<TEntity> : IEntityRepositorySoftDeleted<Guid, TEntity>
where TEntity : class, IKeyedEntity<Guid>, ISoftDeleted, new()
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using OutOfSchool.Services.Models;

namespace OutOfSchool.Services.Repository;

public class SensitiveEntityRepositorySoftDeleted<T> : EntityRepositorySoftDeleted<Guid, T>, ISensitiveEntityRepositorySoftDeleted<T>
VadymLevkovskyi marked this conversation as resolved.
Show resolved Hide resolved
where T : class, IKeyedEntity<Guid>, ISoftDeleted, new()
{
/// <summary>
/// Initializes a new instance of the <see cref="SensitiveEntityRepositorySoftDeleted{T}"/> class.
/// </summary>
/// <param name="dbContext">OutOfSchoolDbContext.</param>
public SensitiveEntityRepositorySoftDeleted(OutOfSchoolDbContext dbContext)
: base(dbContext)
{
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class DirectionServiceTests
{
private DbContextOptions<OutOfSchoolDbContext> options;
private OutOfSchoolDbContext context;
private IEntityRepository<long, Direction> repo;
private IEntityRepositorySoftDeleted<long, Direction> repo;
private IWorkshopRepository repositoryWorkshop;
private IDirectionService service;
private Mock<IStringLocalizer<SharedResource>> localizer;
Expand All @@ -44,7 +44,7 @@ public void SetUp()
options = builder.Options;
context = new OutOfSchoolDbContext(options);

repo = new EntityRepository<long, Direction>(context);
repo = new EntityRepositorySoftDeleted<long, Direction>(context);
repositoryWorkshop = new WorkshopRepository(context);
localizer = new Mock<IStringLocalizer<SharedResource>>();
logger = new Mock<ILogger<DirectionService>>();
Expand Down Expand Up @@ -170,9 +170,7 @@ public async Task Update_WhenEntityIsValid_UpdatesExistedEntity()
Title = "ChangedTitle1",
};

var expected = (await repo.GetByFilter(
d => !d.IsDeleted && d.Id == changedEntity.Id)
.ConfigureAwait(false)).SingleOrDefault();
var expected = await repo.GetById(changedEntity.Id).ConfigureAwait(false);

mapper.Setup(m => m.Map<Direction>(changedEntity)).Returns(expected);
mapper.Setup(m => m.Map<DirectionDto>(expected)).Returns(changedEntity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class StatisticServiceTest

private Mock<IApplicationRepository> applicationRepository;
private Mock<IWorkshopRepository> workshopRepository;
private Mock<IEntityRepository<long, Direction>> directionRepository;
private Mock<IEntityRepositorySoftDeleted<long, Direction>> directionRepository;
private Mock<IMapper> mapper;
private Mock<ICacheService> cache;
private Mock<IAverageRatingService> averageRatingServiceMock;
Expand All @@ -38,7 +38,7 @@ public void SetUp()
{
applicationRepository = new Mock<IApplicationRepository>();
workshopRepository = new Mock<IWorkshopRepository>();
directionRepository = new Mock<IEntityRepository<long, Direction>>();
directionRepository = new Mock<IEntityRepositorySoftDeleted<long, Direction>>();
var logger = new Mock<ILogger<StatisticService>>();
mapper = new Mock<IMapper>();
cache = new Mock<ICacheService>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public async Task<SearchResult<AchievementDto>> GetByFilter(AchievementsFilter f
skip: filter.From,
take: filter.Size,
includeProperties: "Children",
where: predicate)
whereExpression: predicate)
.ToListAsync()
.ConfigureAwait(false);

Expand Down
Loading
Loading