4

I created a generic repository class that all my other repository classes are inheriting from. This is great, because it means almost all the plumbing is done one time for all repositories. I put a full explanation of what I'm talking about here, but here is the code for my GenericRepository (some code is removed for brevity):

public abstract class GenericRepository<T> : IGenericRepository<T> where T : class, new()
{
    private IMyDbContext _myDbContext;

    public GenericRepository(IMyDbContext myDbContext)
    {
        _myDbContext = myDbContext;
    }

    protected IMyDbContext Context
    {
        get
        {
            return _myDbContext;
        }
    }

    public IQueryable<T> AsQueryable()
    {
        IQueryable<T> query = Context.Set<T>();
        return query;
    }

    public virtual void Create(T entity)
    {
        Context.Set<T>().Add(entity);
    }

    public virtual void Update(T entity)
    {
        Context.Entry(entity).State = System.Data.EntityState.Modified;
    }
}

As you see, I have a Create method and an Update method. It would be very convenient to have a "CreateOrUpdate" method, so I don't have to manually check for existing objects each time I have to save something to the database.

Each of my objects in Entity Framework have an "Id", but the challenge here is that the GenericRepository works with "T".

Now, with that rather long introduction, to my specific question.

How do I create a generic CreateOrUpdate method for my GenericRepository?

UPDATE

After Marcins response, I implemented the following generic methods in my GenericRepository. It will take some time before I can test that it works as expected, but it looks very promising.

public virtual bool Exists(Guid id)
{
    return Context.Set<T>().Any(t => t.Id == id);
}

public virtual void CreateOrUpdate(T entity)
{
    if (Exists(entity.Id))
    {
        var oldEntity = GetSingle(entity.Id);
        Context.Entry(oldEntity).CurrentValues.SetValues(entity);
        Update(oldEntity);
    }
    else
    {
        Create(entity);
    }
}

The code above has no less than 3 roundtrips to the database when updating. I'm sure it can be optimized, but it wasn't really the exercise for this question.

This question handles that topic better: An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key

Community
  • 1
  • 1
Niels Brinch
  • 3,033
  • 9
  • 48
  • 75
  • 1
    You make a trip to the database to decide if the entity needs to be inserted or updated. You only need to check whether the entity's Id is the default value for a Guid. (`entity.ID == default(Guid)`). Also you may want to ensure proxies are always created if you intend to use lazy-loading. See this example: http://stackoverflow.com/a/16811976/150342 – Colin Aug 19 '13 at 08:18
  • Some business logic may want to set a Guid as primary key early on while creating, so I would have to make the little roundtrip to the database in my case. Otherwise a very good point, which is relevant in most other cases. Thanks. – Niels Brinch Aug 19 '13 at 12:26
  • Actually, my implementation fails with the following error whenever I try to update `An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.`. It happens when sending a new object with the same Guid as an existing object. It's not related to this question, but just thought I would let you know in case someone else wanted to use this code. – Niels Brinch Aug 19 '13 at 15:40
  • Whats wrong with letting the caller decide if it needs to create or update. Why add the logic to the api/app/service layers? – I Stand With Russia Aug 21 '17 at 22:10
  • Because the UI may not have an opinion on whether it is creating or updating, it is simply saving some information and is doing so in the same way whether it is creating or updating. So it saves a lot of logic work in the frontend to have a single method to handle both. There are probably other reasons. – Niels Brinch Sep 01 '17 at 08:53

1 Answers1

4

Create a interface with Id property, implement it on every of your entities and add another generic constraint to your class:

public interface IEntity
{
    int Id { get; set;}
}

And

public abstract class GenericRepository<T> : IGenericRepository<T> where T : class, IEntity, new()

With that, you'll be able to use Id property within your generic repository class.

Of course - Id don't have to be an int, it can be Guid as well.

MarcinJuraszek
  • 124,003
  • 15
  • 196
  • 263