Prompt Expression<Func<T, bool>> via parameter using object that is in a foreach

Asked

Viewed 563 times

8

I have a model called Entity, this model has links (1 - N) with three other models.

public class Entity
{
    // Outras propriedades removidas para brevidade
    public virtual List<SpecificInfo> SpecificInfo { get; set; }
    public virtual List<EntityContact> Contacts { get; set; }
    public virtual List<EntityAddress> Addresses { get; set; }
}

On a certain occasion, a method (let’s call Edit) receives an instance of this model and needs to verify which properties have been modified (based on the model in the database). In the case of these three properties a more detailed check is necessary, since they are lists of objects where I need to check among all the items in the list which have been added, changed or deleted (this comparing to another list, see below).

Example of how the method is Edit:

private void Edit(Entity model)
{
    //Início do código removido

    var existentSpecificInfo = _db.EntitiesSpecificInfo.Where(info => info.EntityId == id).ToList();
    var validatedSpecificInfo = new List<EntitySpecificInfo>();

    foreach (var info in model.Entity.SpecificInfo)
    {
        var existentInfo = existentSpecificInfo.SingleOrDefault(x => x.Description == info.Description);

        if (existentInfo != null)
        {
            info.Id = existentInfo.Id;
            _db.Entry(existentInfo).State = EntityState.Detached;
            _db.Entry(info).State = EntityState.Modified;
            validatedSpecificInfo.Add(existentInfo);
        }
        else
        {
            _db.Entry(info).State = EntityState.Added;
        }
    }

    existentSpecificInfo.RemoveAll(x => validatedSpecificInfo.Contains(x));
    existentSpecificInfo.ForEach(x => _db.Entry(x).State = EntityState.Deleted);

    //Verificar os contatos enviados
    var existentContacts = _db.EntitiesContacts.Where(x => x.EntityId == id).ToList();
    var validatedExistentContacts = new List<EntityContact>();

    foreach (var contact in model.Entity.Contacts)
    {
        var existentContact = existentContacts.SingleOrDefault(x => x.Id == contact.Id);

        if (existentContact != null)
        {
            contact.Id = existentContact.Id;
            _db.Entry(existentContact).State = EntityState.Detached;
            _db.Entry(contact).State = EntityState.Modified;
            validatedExistentContacts.Add(existentContact);
        }
        else
        {
            _db.Entry(contact).State = EntityState.Added;
        }
    }

    existentContacts.RemoveAll(x => validatedExistentContacts.Contains(x));
    existentContacts.ForEach(x => _db.Entry(x).State = EntityState.Deleted);

    //Verificar os endereços enviados
    var existentAddresses = _db.EntitiesAddresses.Where(x => x.EntityId == id).ToList();
    var validatedExistentAddresses = new List<EntityAddress>();

    foreach (var address in model.Entity.Addresses)
    {
        var existentAddress = existentAddresses.SingleOrDefault(x => x.Id == address.Id);

        if (existentAddress != null)
        {
            address.Id = existentAddress.Id;
            _db.Entry(existentAddress).State = EntityState.Detached;
            _db.Entry(address).State = EntityState.Modified;
            validatedExistentAddresses.Add(existentAddress);
        }
        else
        {
            _db.Entry(address).State = EntityState.Added;
        }
    }

    existentAddresses.RemoveAll(x => validatedExistentAddresses.Contains(x));
    existentAddresses.ForEach(x => _db.Entry(x).State = EntityState.Deleted);
}

It turns out, as you can see, practically the same block of code is repeated three times, and it does basically the same thing.

I thought of making a generic method, where I could leave all the code repeated and pass the different parts by parameter.

What I’ve done so far is like this:

public void Test<T>(IEnumerable<T> infoList, Func<T, bool> selector) where T : class
{
    var existentInfo = _db.Set<T>().Where(selector).ToList(); 
    var validatedInfo = new List<T>();

    foreach (var info in infoList)
    {
        var existentAttr = existentInfo
                           .SingleOrDefault(x => x.Description == info.Description); 
                           // Vide obs abaixo

        if (existentAttr != null)
        {
            info.Id = existentAttr.Id;
            _db.Entry(existentAttr).State = EntityState.Detached;
            _db.Entry(info).State = EntityState.Modified;
            validatedInfo.Add(existentAttr);
        }
        else
        {
            _db.Entry(info).State = EntityState.Added;
        }
    }

    existentInfo.RemoveAll(x => validatedInfo.Contains(x));
    existentInfo.ForEach(x => _db.Entry(x).State = EntityState.Deleted);
}

// O uso seria algo como:

Test<SpecificInfo>(model.SpecificInfo, (inf => inf.EntityId == model.Id));
Test<EntityContact>(model.Contacts, (c => c.EntityId == model.Id));
Test<EntityAddress>(model.Addresses, (ad => ad.EntityId == model.Id));

The excerpt existentInfo.SingleOrDefault(x => x.Description == info.Description); obviously accuses a build error, after all the property Description does not exist within T, may even exist, but the compiler has no way of knowing. Of course I could create an interface and restrict the execution of the method to classes that implement this interface, the problem is that I can’t "hardcodar" to Expression, because in each case Expression should use different properties, see the method Edit:

[...].SingleOrDefault(x => x.Description == info.Description); // 1º bloco
[...].SingleOrDefault(x => x.Id == contact.Id); // 2º bloco
[...].SingleOrDefault(x => x.Id == address.Id); // 3º bloco

This could also be solved by receiving the expression via parameter, as is done with the variable selector, the problem is that I have no idea how to do this expression being that I need to use the object that is inside the foreach.

  1. Is there any way to parameterize this expression that goes into the SingleOrDefault? - Taking into account the circumstances set out above.

  2. If not, is there any way I can improve on this method and avoid so much repetition?

  3. Is there any other approach I can use that will help me solve this problem?

2 answers

5


I will need to divide this answer into two: the first part will talk about traditional Linq. The second part will talk about Entity Framework.

Linq Tradicional

I rode this Fiddle explaining how it can be done using a dynamic property. There is not much secret: using Reflection, we ask for the name of the property based on the type (in your case, T) and compare the values.

Only that the IQueryable of a DbSet builds an SQL sentence from the predicate, and most likely using Reflection in the predicate will not work, so you will have to build a sentence dynamically...

Using System.Linq.Dynamic

A great complement to traditional Linq, allows the use of dynamic expressions when assembling your IQueryable.

Install the Nuget package and use like this:

var name = "Description";
var existentInfo = existentSpecificInfo.Where(name + "==@0", info.Description).SingleOrDefault(); // É assim mesmo. Não tem SingleOrDefault neste pacote.
  • The first option already suits me, because the selection I make is in a list in memory. Thank you, was as I wanted =).

2

as a suggestion, you can try to make the .GroupJoin to make a Full Join between your entities in memory and those of the context.

public static void Edit<T, TKey>(this DbContext _db, IEnumerable<T> infoList, Expression<Func<T, TKey>> chave, Expression<Func<T, bool>> filtro) where T : class
{
    var dbSet = _db.Set<T>().Where(filtro);
    var infos = infoList.AsQueryable();
    var left  = infos.GroupJoin(dbSet, chave, chave, (info, existent) => new { Existent = existent.SingleOrDefault(), Info = info });
    var right = dbSet.GroupJoin(infos, chave, chave, (existent, info) => new { Existent = existent, Info = info.SingleOrDefault() });

    foreach (var entry in left.Union(right))
    {
        if (entry.Existent == default(T))
        {
            _db.Entry(entry.Info).State = EntityState.Added;
        }
        else if (entry.Info == default(T))
        {
            _db.Entry(entry.Existent).State = EntityState.Deleted;
        }
        else
        {
            _db.Entry(entry.Existent).State = EntityState.Detached;
            _db.Entry(entry.Info).State = EntityState.Modified;
        }
    }
}

Then test the following call (I had no way to test here):

_db.Edit(model.SpecificInfo, info => info.Description, info => info.EntityId == id);
_db.Edit(model.EntitiesContacts, contact => contact.Id, info => info.EntityId == id);
_db.Edit(model.EntitiesAddresses, address => address.Id, info => info.EntityId == id);

Browser other questions tagged

You are not signed in. Login or sign up in order to post.