4

I have a reoccuring code block in my EntityFramework backed repository which I would like to genericise somehow and call as a method, so reuse the code rather than repeat it.

The current code block looks like this:

        // Archive deleted MyItems sections
        _t.MyItems.Where(x => x.ValidTo == null && !team.MyItems.Contains(x)).ToList().ForEach(x => x.ValidTo = DateTime.Now);

        // Add or update MyItems sections
        foreach (var MyItemsSection in team.MyItems)
        {
            if (MyItemsSection.Id == default(int))
            {
                MyItemsSection.ValidFrom = DateTime.Now;
                _t.MyItems.Add(MyItemsSection);
            }
            else
            {
                var _MyItemsSection = _t.MyItems.FirstOrDefault(x => x.Id == MyItemsSection.Id);
                context.Entry(_MyItemsSection).CurrentValues.SetValues(MyItemsSection);
            }
        }

_t is the EntityFramework connected object graph, while team is an identical type of object graph that has been disconnected and possibly updated externally. The goal here is to sync the two object graphs so the changes are persisted.

I need to pass in _t.MyItems and team.MyItems, where MyItems are to be genericised so the same method works for MyOtherItems and MySocks, MyUnderPants etc.

Is this at all possible?

3
  • 1
    If we assume MyItems is a collection of T, do all other T's have properties Id and ValidTo? That seems to be the sticking point to making this more generic and work with different collections Commented Nov 23, 2012 at 15:40
  • I think you should post this on codereview: codereview.stackexchange.com Commented Nov 23, 2012 at 15:44
  • Yes, the all have Id and ValidTo, but context.Entry().CurrentValues.SetValues() still needs to work as well, so an interface based on just those two properties won't work. While I don't know how to do it, I'm pretty sure I need to pass the types in as well. Commented Nov 23, 2012 at 15:51

2 Answers 2

1

You have two choices: Either you constrain your objects to a known base type which contains the properties and methods you want to access in the generic method or you use predicates to do the selection.

Constraints:

// base type
interface IFoo {
  int ID { get; set; }
}

  // generic method
  public List<T> Update<T>(List<T> graph1, List<T> graph2) where T : IFoo {
    var update = graph1.Intersect(graph2, (g1, g2) => { g1.ID == g2.ID }).ToList();
    return update;
  }

Predicates:

public void Update<T, U>(T _t, T team, Func<T, IList<U>> selector) 
{
    var _tItems = selector(_t);
    var teamItems = selector(team);

    // Archive deleted MyItems sections
    _tItems.Where(x => x.ValidTo == null && !teamItems.Contains(x)).ToList().ForEach(x => x.ValidTo = DateTime.Now);

    // Add or update MyItems sections
    foreach (var MyItemsSection in teamItems)
    {
        if (MyItemsSection.Id == default(int))
        {
            MyItemsSection.ValidFrom = DateTime.Now;
            _tItems.Add(MyItemsSection);
        }
        else
        {
            var _MyItemsSection = _tItems.FirstOrDefault(x => x.Id == MyItemsSection.Id);
            context.Entry(_MyItemsSection).CurrentValues.SetValues(MyItemsSection);
        }
    }
}

    //Usage:
    Update(_t, team, (t => t.MyItems));

But then again, what keeps you from writing a method which takes the lists as parameters?

As in public void Update<T>(IList<T> _tItems, IList<T> teamItems)

Sign up to request clarification or add additional context in comments.

7 Comments

Constraining to a base type won't work, because they are going to be different - the structure I want to genericise is more a product of how EF works than my own doing, so EF still has to be able to do its thing. I don't know whether you noticed, but my code example has three things going on - archiving of deleted items, adding of new items and updating of existing items. EF makes just merging the object graphs problematic for various reasons.
@Moo - You can constrain them to a base interface, which you can add to the objects either by modifying the T4 template or in a new file with another part of the partial class object. That won't interfere with EF at all, and if EF is already generating the properties which the interface requires, you don't have to do anything else.
I understand that, but won't context.Entry().CurrentValues.SetValues() be getting a type other than the type it expects (ie, the entity)? As you can just pass the entity in to both Entry() and SetValues(), I'm not entirely convinced that passing in a constrained type won't cause EF to miss the fact entirely that I'm passing in two entity types that it knows about. Does that make sense?
I see what you're saying, and I'm not entirely sure of the answer. I think the real issue is with _t.MyItems - since you don't know which type MyItem will be, you don't know which table/property to look in to find it. Once you found it, you should be able to work with it just fine, but finding it would be hard. As far as I know, at least - I haven't done anything extensive with EF, so I could be mistaken.
Thanks for the answer, but I'm starting to think that this is one of those times when any generic form will look more like encrypted porn than readable code, if its at all possible anyway :) I will know what type MyItem will be when I pass it in, so if I can pass in the types and the objects then something might be possible there...?
|
1

In answer to my own question, here is the answer - what I was missing was the fact that you can require the incoming type as implementing a specific interface, and still have it available as the type wanted.

So, here is what I came up with:

public void UpdateEntities<TEntity>(ICollection<TEntity> pocoCollection, ICollection<TEntity> dbCollection)
        where TEntity : class, IEntity
    {
        // Archive deleted entities
        dbCollection.Where(x => x.ValidTo == null && !pocoCollection.Contains(x)).ToList().ForEach(x => x.ValidTo = DateTime.Now);

        // Add or update entities
        foreach (var entity in pocoCollection)
        {
            if (entity.Id == default(int))
            {
                entity.ValidFrom = DateTime.Now;
                dbCollection.Add(entity);
            }
            else
            {
                var _entity = dbCollection.FirstOrDefault(x => x.Id == entity.Id);
                context.Entry(_entity).CurrentValues.SetValues(entity);
            }
        }
    }

The part which I was looking for was the where TEntity : class, IEntity

In this solution, I have to make sure that my entities implement the interface IEntity, which simply is:

public interface IEntity
{
    int Id { get; set;}
    DateTime ValidFrom { get; set; }
    DateTime? ValidTo { get; set; }
}

This allows the compiler to quit complaining about the use of those properties, while I can still use the actual type so Entity Framework is also satisfied and left less confused about whats going on.

Hope this helps someone else.

Comments

Your Answer

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