I'm in a .NET 6 web application with EFCore, and I have these boilerplate abstract classes and interfaces for defining entities. I know it's quite a number of complexity layers, but it really helps me reduce the workload when adding new entities and when writing stores and managers. There are also attributes, but it's all been deleted for convenience:
public abstract class ResourceBase<T> : ResourceBase<T, Guid>
{
}
public abstract class ResourceBase<T, UKey> : ReadonlyResourceBase<T, UKey>
{
public virtual string CreateUser { get; set; } = string.Empty;
public virtual DateTime CreateDate { get; set; }
}
public abstract class ReadonlyResourceBase<T, UKey> : IResource<UKey>, IEquatable<T>
{
public virtual UKey Id { get; set; } = default(UKey)!;
public virtual string Name { get; set; } = string.Empty;
public abstract bool Equals(T? other);
public new abstract int GetHashCode();
}
public interface IResource<TKey> : INamelessResource<TKey>
{
string Name { get; }
}
public interface INamelessResource<TKey>
{
TKey Id { get; }
}
Now, I have the entities themselves. Let's say this one that whose comparison is very simple:
public class Istituto : ResourceBase<Istituto>
{
// properties
public override bool Equals(Istituto? other)
{
ReferenceEquals(null, other) ? false : this.Name == other.Name;
}
public override int GetHashCode() => Name.GetHashCode();
}
Then, in the store class, I have to check whether an equal entry already exists in the database:
public async Task<OperationResult> CreateAsync(Istituto resource)
{
Istituto? istituto = await context.Istituti.FirstOrDefaultAsync(e => e.Equals(resource));
if (istituto != null)
{
return OperationResult.Failure("Impossibile registrare. Istituto già esistente");
}
try
{
context.Istituti.Add(resource);
await context.SaveChangesAsync();
return OperationResult.Success(resource);
}
catch (Exception ex)
{
return OperationResult.Failure(ex.InnerException?.Message ?? ex.Message);
}
}
The problem is that my version of Equals in the lambda expression inside the .FirstOrDefaultAsync() isn't being called at all.
If I try this more verbose (and underperforming, I guess?) approach, it works:
List<Istituto> istituti = await context.Istituti.ToListAsync();
bool equal = false;
foreach(Istituto istituto in istituti)
{
equal = istituto.Equals(resource);
if (equal)
{
return OperationResult.Failure("Impossibile registrare. Istituto già esistente");
}
}
I really can't understand why, any help would be greatly appreciated.
I know it's quite a number of complexity layers, but it really helps me reduce the workloadquite the opposite. Not only is all this code unnecessary and causes the current problem, it can cause serious problems with change tracking, which depends on actual object equality. It also breaks optimistic concurrency. Thatawait context.SaveChangesAsync();can easily execute 42 pending DELETEs and 67 UPDATEs along with the single insert. Looks like an attempt to implement the pseudo-repository antipattern.Names, so do that in the query. And stop usingCreateAsyncor any other unfortunate low-level CRUD methods. You're not using a DAO. EF Core, NHibernate and other full-featured ORMs load, track and persist entire graph of objects at once.Nametoo. And probably create a UNIQUE constraint, it you expect names to be unique.