Search code examples
c#linqgenericsexpression-treesfunc

Providing a generic key comparison based on a collection of a generic type


I have created my own InsertOrUpdate() implementations for a few types like this:

public IEnumerable<Genre> InsertOrUpdate(IEnumerable<Genre> genres)
{
    foreach (var genre in genres)
    {
        var existingGenre = _context.Genres.SingleOrDefault(x => x.TmdbId == genre.TmdbId);
        if (existingGenre != null)
        {
            existingGenre.Update(genre);
            yield return existingGenre;
        }
        else
        {
            _context.Genres.Add(genre);
            yield return genre;
        }
    }
    _context.SaveChanges();
}

The return type of IEnumerable<T> is required because it will be used to insert the root object in the datacontext. This method basically retrieves the attached object if it exists and updates it with the newest values if it does or inserts it as a new object if it doesn't. Afterwards this attached object is returned so it can be linked to the root object in the many-to-many tables.

The problem now is that I have several of these collections (genres, posters, keywords, etc) and each type's ID is differently setup: sometimes it's called TmdbId, sometimes Id and sometimes Iso. It's one thing to use an interface and rename them all to Id but the problem exists in that they are also different types: some are int and some are string.

The question is easy: how I do turn this into something more generic to avoid this code duplication?

So far I have been toying around with

public IEnumerable<T> InsertOrUpdate<T>(IEnumerable<T> entities, Func<T, bool> idExpression) where T : class 
{
    foreach (var entity in entities)
    {
        var existingEntity = _context.Set<T>().SingleOrDefault(idExpression);
        if (existingEntity != null)
        {
            _context.Entry(existingEntity).CurrentValues.SetValues(entity);
            yield return existingEntity;
        }
        else
        {
            _context.Set<T>().Add(entity);
            yield return entity;
        }
    }
    _context.SaveChanges();
}

but obviously this won't work since I have no access to the inner entity variable. Sidenote: IDbSet<T>().AddOrUpdate() does not work in my scenario.


Solution

  • You could try:

    public IEnumerable<T> InsertOrUpdate<T>(IEnumerable<T> entities, Func<T, object[]> idExpression) where T : class
    

    and

    var existingEntity = _context.Set<T>().Find(idExpression(entity));
    

    called with something like

    movie.Genres = new List<Genre>(InsertOrUpdate(movie.Genres, x => new object[] { x.Id }));
    

    (note that a method that returns a IEnumerable<> is very dangerous... If you don't enumerate it, like

    InsertOrUpdate(movie.Genres, x => x.Id);
    

    then the method won't be executed fully, because it will be lazily executed "on demand")

    If you only have single-key tables, you can change it to:

    public IEnumerable<T> InsertOrUpdate<T>(IEnumerable<T> entities, Func<T, object> idExpression) where T : class
    

    and

    var existingEntity = _context.Set<T>().Find(new object[] { idExpression(entity) });
    

    and

    movie.Genres = new List<Genre>(InsertOrUpdate(movie.Genres, x => x.Id));