My repository class has a few methods for operations with the database. I recently developed the FindAsync()
method, which allows you to grab only a few rows from the database based on an expression.
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Orbit.Infrastructure.Repositories.Interfaces;
using System.Linq.Expressions;
namespace Orbit.Infrastructure.Repositories
{
public class Repository<TEntity> : IRepository<TEntity> where TEntity : class
{
protected readonly DbContext Context;
public Repository(DbContext context, IConfiguration configuration)
{
IConfigurationSection sectNamespaces = configuration.GetSection("AllowedEntityNamespaces");
List<string> namespacesInConfig = [];
foreach (IConfigurationSection child in sectNamespaces.GetChildren())
{
namespacesInConfig.Add(child.Path);
}
if (namespacesInConfig.Contains(typeof(TEntity).Namespace!))
{
throw new ArgumentException($"O namespace ${typeof(TEntity).Namespace} não está habilitado!");
}
Context = context;
}
public async Task AddAsync(TEntity entity)
{
ArgumentNullException.ThrowIfNull(nameof(entity));
_ = await Context.Set<TEntity>().AddAsync(entity);
}
public async Task AddRangeAsync(IEnumerable<TEntity> entities)
{
ArgumentNullException.ThrowIfNull(nameof(entities));
await Context.Set<TEntity>().AddRangeAsync(entities);
}
public async Task<IEnumerable<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate)
{
ArgumentNullException.ThrowIfNull(predicate);
return await Context.Set<TEntity>().Where(predicate).ToListAsync();
}
public async Task<IEnumerable<TEntity>> GetAllAsync()
{
return await Context.Set<TEntity>().ToListAsync();
}
public async Task<IEnumerable<TEntity>> GetAllAsync(params string[] navProperties)
{
IQueryable<TEntity> entities = Context.Set<TEntity>().AsQueryable();
foreach (string prop in navProperties)
{
entities = entities.Include(prop);
}
return await entities.ToListAsync();
}
public async Task<TEntity?> GetAsync(int id)
{
ArgumentNullException.ThrowIfNull(id);
return await Context.Set<TEntity>().FindAsync(id);
}
public async Task RemoveAsync(TEntity entity)
{
ArgumentNullException.ThrowIfNull(entity);
await Task.Run(() =>
{
_ = Context.Set<TEntity>().Remove(entity);
});
}
public async Task RemoveRangeAsync(IEnumerable<TEntity> entities)
{
ArgumentNullException.ThrowIfNull(entities);
await Task.Run(() =>
{
Context.Set<TEntity>().RemoveRange(entities);
});
}
}
}
Up until now, in most operations where I needed to grab one or more users based on a specific criterion, I pulled in all the users (using GetAllAsync()
) and only then did I filter the users using LINQ. I realized that this was not the best solution. Ideally, you would want to throw this load of pulling a few specific users into the efcore and balance the load on the application.
In my class of services I also developed a method that allows you to find a user based on a predicate and that's where the problem lies.
UserAddRequest
(DTO)
using Orbit.Domain.Entities;
using System.ComponentModel.DataAnnotations;
namespace Orbit.Application.Dtos.Requests
{
public class UserAddRequest
{
[Required(ErrorMessage = "Insira o nome do usuário!")]
[StringLength(100, MinimumLength = 5, ErrorMessage = "O nome do usuário deve ter no máximo 100 caracteres.")]
[RegularExpression(@"^[a-zA-Z0-9_]*$", ErrorMessage = "O nome do usuário só pode conter letras, números e underline.")]
[Display(Name = "Name")]
public string UserName { get; set; } = null!;
[Required(ErrorMessage = "Insira o email do usuário!")]
[StringLength(200, ErrorMessage = "O email do usuário deve ter no máximo 200 caracteres.")]
[EmailAddress(ErrorMessage = "O email do usuário não é válido.")]
[Display(Name = "Email")]
public string UserEmail { get; set; } = null!;
[Required(ErrorMessage = "Insira a data de nascimento do usuário!")]
[DataType(DataType.Date, ErrorMessage = "Formato de data inválido.")]
[Display(Name = "Data de Nascimento")]
public DateOnly UserDateOfBirth { get; set; }
[Required(ErrorMessage = "Insira a senha do usuário!")]
[StringLength(200, ErrorMessage = "A senha do usuário deve ter no máximo 200 caracteres.")]
[RegularExpression(@"^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,}$", ErrorMessage = "A senha deve conter pelo menos uma letra minúscula, uma letra maiúscula, um caractere especial (@$!%*?&)")]
[Display(Name = "Senha")]
public string UserPassword { get; set; } = null!;
[Required(ErrorMessage = "O campo {0} é obrigatório.")]
[Range(1, 31, ErrorMessage = "O campo {0} deve estar entre 1 e 31.")]
[Display(Name = "Dia")]
public int Day { get; set; }
[Required(ErrorMessage = "O campo {0} é obrigatório.")]
[Range(1, 12, ErrorMessage = "O campo {0} deve estar entre 1 e 12.")]
[Display(Name = "Mês")]
public int Month { get; set; }
[Required(ErrorMessage = "O campo {0} é obrigatório.")]
[Range(1, 9999, ErrorMessage = "O campo {0} deve estar entre 1 e 9999.")]
[Display(Name = "Ano")]
public int Year { get; set; }
public User ToUser()
{
return new User
{
UserName = UserName,
UserDateOfBirth = new DateOnly(Year, Month, Day),
UserEmail = UserEmail,
UserPassword = UserPassword
};
}
}
}
UserResponse
(DTO)
using Orbit.Domain.Entities;
namespace Orbit.Application.Dtos.Responses
{
public class UserResponse
{
public uint UserId { get; set; }
public string UserName { get; set; } = null!;
public string UserEmail { get; set; } = null!;
public DateOnly UserDateOfBirth { get; set; }
public string UserPassword { get; set; } = null!;
public string? UserDescription { get; set; }
public byte[]? UserImageByteType { get; set; }
public string? UserProfileName { get; set; }
public ICollection<UserResponse>? Followers { get; set; }
public ICollection<UserResponse>? Users { get; set; }
}
public static class UserExtensions
{
public static UserResponse ToUserResponse(this User user)
{
return user.ToUserResponseInternal([]);
}
private static UserResponse ToUserResponseInternal(this User user, List<uint> processedUserIds)
{
if (processedUserIds.Contains(user.UserId))
{
return new UserResponse
{
UserId = user.UserId,
UserName = user.UserName,
UserEmail = user.UserEmail,
UserDateOfBirth = user.UserDateOfBirth,
UserPassword = user.UserPassword,
UserDescription = user.UserDescription,
UserProfileName = user.UserProfileName,
UserImageByteType = user.UserImageByteType,
Followers = [],
Users = []
};
}
processedUserIds.Add(user.UserId);
UserResponse response = new()
{
UserId = user.UserId,
UserName = user.UserName,
UserEmail = user.UserEmail,
UserDateOfBirth = user.UserDateOfBirth,
UserPassword = user.UserPassword,
UserDescription = user.UserDescription,
UserProfileName = user.UserProfileName,
UserImageByteType = user.UserImageByteType,
Followers = user.Followers.Select(u => u.ToUserResponseInternal(new List<uint>(processedUserIds))).ToList(),
Users = user.Users.Select(u => u.ToUserResponseInternal(new List<uint>(processedUserIds))).ToList()
};
return response;
}
}
}
User model (EF Core and repository model):
namespace Orbit.Domain.Entities;
public partial class User
{
public uint UserId { get; set; }
public string UserName { get; set; } = null!;
public string UserEmail { get; set; } = null!;
public DateOnly UserDateOfBirth { get; set; }
public string UserPassword { get; set; } = null!;
public string? UserDescription { get; set; }
public byte[]? UserImageByteType { get; set; }
public string? UserProfileName { get; set; }
public virtual ICollection<User> Followers { get; set; } = [];
public virtual ICollection<User> Users { get; set; } = [];
}
My FindUserAsync
method of the UserService
class has an Expression<Func<UserResponse, bool>>
as a parameter, and the FindAsync
method of the repository takes an Expression<Func<TEntity, bool>>
as an argument. In the service class, the generic TEntity
is actually the User
model
public async Task<IEnumerable<UserResponse>> FindUsersAsync(Func<UserResponse, bool> predicate)
{
// This isn't allowed. Causes compilation error
return await _unitOfWork.User.FindAsync(predicate);
}
How can I convert one expression to another? Is there a way to convert the expressions or use string to map? I don't have the slightest idea how to proceed from here.
I've tried to find some solutions on the internet, but I haven't found anything that I understand in the slightest bit about what's going on.
Thanks!
DbSet<T>
already provides.