Ask ExpressionFuncT, bool via parameter using object that is in a foreach

8

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

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 given occasion, a method (let's call Edit ) receives an instance of this model and needs to check which properties have been modified (based on the model that is 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 which of the items in the list have been added, changed or deleted (compare this with another list, see below).

Example of how method Edit is:

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 that, as you can see, practically the same block of code is repeated three times, and it does basically the same thing.

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

What I've done so far, looks 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 existentInfo.SingleOrDefault(x => x.Description == info.Description); obviously has a compile error, after all the Description property does not exist within T , it 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 for classes that implement this interface, the problem is that I can not "hardcodar" the expression, because in each case the expression must use different properties , see the Edit method:

[...].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 getting 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.

  • Is there any way to parameterize this expression that goes inside SingleOrDefault ? - Taking into account the circumstances presented above.

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

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

  • asked by anonymous 03.06.2016 / 21:22

    2 answers

    5

    I'll need to split this answer in two: the first part will talk about traditional Linq. The second part will talk about Entity Framework.

    Traditional Linq

    I mounted 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 this case, T ) and compare the values.

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

    p>

    Using System.Linq.Dynamic

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

    Install the NuGet package and use the following:

    var name = "Description";
    var existentInfo = existentSpecificInfo.Where(name + "==@0", info.Description).SingleOrDefault(); // É assim mesmo. Não tem SingleOrDefault neste pacote.
    
        
    03.06.2016 / 23:09
    2

    As a suggestion, you can try to make .GroupJoin to make a Full Join between your in-memory entities and those in 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 try 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);
    
        
    04.06.2016 / 02:50