0

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.

6
  • 4
    Entity Framework converts expression lambdas into SQL. Whatever database you're targetting, it's notion of equality is not affected by any of your C# code. Commented Sep 9 at 6:52
  • 1
    I know it's quite a number of complexity layers, but it really helps me reduce the workload quite 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. That await 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. Commented Sep 9 at 6:59
  • @Damien_The_Unbeliever I thought that still would work, being that entity properties are being compared. So I guess I'm forced to load the whole entity in memory? Commented Sep 9 at 6:59
  • @DavideVitali no, the solution is to stop using this antipattern. You want to compare Names, so do that in the query. And stop using CreateAsync or 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. Commented Sep 9 at 7:19
  • Performance is affected by the indexes in the database, not what client applications say. Comparing by Id is fast assuming that's the primary key or covered by an index. To get fast performance when comparing names you need to index Name too. And probably create a UNIQUE constraint, it you expect names to be unique. Commented Sep 9 at 7:22

1 Answer 1

1

When you override Equals, EF can't translate your custom Equals logic into SQL. Here are three recommended approaches:

1.If your Equals method is based on Name, you can directly compare that property :

Istituto? istituto = await context.Istituti.FirstOrDefaultAsync(e => e.Name == resource.Name);

2.Use AsEnumerable() to handle it in memory, but this is inefficient for large datasets :
Istituto? istituto = context.Istituti.AsEnumerable().FirstOrDefault(e => e.Equals(resource));

3.Create a custom extension method for consistent logic :


public static class IstitutoExtensions 
{ 
    public static Expression<Func<Istituto, bool>> EqualsByName(string name) => 
        e => e.Name == name; 
} 

Istituto? istituto = await context.Istituti
   .FirstOrDefaultAsync(IstitutoExtensions.EqualsByName(resource.Name));
Sign up to request clarification or add additional context in comments.

1 Comment

Another way to write the extension is as an IQueryable eg public static IQueryable<Istituto> EqualsByName(this IQueryable<Istituto> source, string name) => source.Where(e => e.Name == name);

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.