-1

While it feels that this question should have been answered many times already, I can not find anything useful (except for BulkInsert addons which I'd like to avoid)

        public virtual void AddRange(ICollection<T> entries)
        {
            _context.Set<T>().AddRange(entries);
        }

        public virtual void UpdateRange(ICollection<T> entries)
        {
            _context.Set<T>().UpdateRange(entries);
        }

So how to combine these 2 methods together? T is a class and has a key "Id" property (or it can have other composite key, thus I want this solution to be truly generic), but I don't want to make an interface out of it (to check if equals to 0 to mark entry as Added, Modified otherwise) as it complicates design.

Nick Farsi
  • 366
  • 4
  • 19
  • A DbSet is already a generic single-entity repository. A DbContext is already a multi-entity repository *and* Unif-of-Work. You don't to tell EF Core explicitly whether a detached object is new or modified, unless you want to override the existing behavior. ORMs are meant to give the impression of working with in-memory objects instead of tables and rows. The most advanced of them don't need "upsert" - that's not even the Repository pattern, it's the more primitive Data Access Object pattern – Panagiotis Kanavos Dec 05 '22 at 15:50
  • 2
    Neither `Add/AddRange` nor `Update/UpdateAsync` write anything to the database. They attach a detached entity in a specific state. If the entities have database-generated keys, [Update](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.update?view=efcore-7.0) will start tracking entities with keys in the Modified state and those without in the Added state. There's no need to call `Add/AddRange` unless the app wants to attach new entities with client-generated keys. All changes will be saved when `SaveChanges` is called – Panagiotis Kanavos Dec 05 '22 at 15:56
  • `if equals to 0 to mark entry as Added, Modified otherwise` that's what EF already does – Panagiotis Kanavos Dec 05 '22 at 15:56
  • ```You don't to tell EF Core explicitly whether a detached object is new or modified``` - ok, let's consider this: I have a table named Onion. I have a list of onions to be upserted to this table. Some of these are already present in db - I have to call Update for these. Some of them are new - I have to call Add for these. How to make EF decide itself whether to call Update or Insert rather than doing it manually? – Nick Farsi Dec 05 '22 at 16:01
  • 1
    That's the wrong mental model. You think of EF as a Data Access Object or a database connection. What you ask is already provided by EF Core. I already posted the link to the docs that explain how EF decides *in what state to track an object* - not what method to call. You can use `Update` with both new and modified objects. If the key is database generated, objects with no key value are considered new. Objects with a key value are considered modified – Panagiotis Kanavos Dec 05 '22 at 16:11
  • If the entity has db-generated keys you *can't* insert key values in the first place. This makes it clear that if an entity's key has specific values, it can only be updated (or there's a logic problem in the app) – Panagiotis Kanavos Dec 05 '22 at 16:12
  • Why can't I? I have external entries that already have their id defined (in fact it is open id), I use the same ids in my DB for PK. What is wrong with that? – Nick Farsi Dec 05 '22 at 18:09
  • Try storing a number in an IDENTITY (SQL Server) or AutoNumber (MySQL) column. You'll get an error from the database itself. Allowing clients to store arbitrary values in automatically generated fields means duplicate values will be stored sooner or later. If the current DB ID is 5 and you store 8, the database will generate 8 too after a while. Nothing prevents multiple clients from generating the same ID either. – Panagiotis Kanavos Dec 05 '22 at 18:12
  • Postgres allows it just fine, I have entries with open ids which I use as primary keys in DB. No duplicates because it's a primary key as well. I just lack a generic upsert method. – Nick Farsi Dec 05 '22 at 18:19
  • No, you avoid duplicates because you'll get an error if you try to insert an already existing value. That's what EF Core itself avoids. You don't need an Upsert, I already explained that `Update` does that job already, and none of these methods updates or inserts anything explicitly. If you insist on thinking of EF Core and Entities as an NpSqlConnection and tables you'll end up writing a ton of code trying to replicate what's already available. – Panagiotis Kanavos Dec 05 '22 at 18:22
  • You asked for something that `check if equals to 0 to mark entry as Added, Modified otherwise`. That's what Update already does: `if an entity has its primary key value set then it will be tracked in the Modified state. If the primary key value is not set then it will be tracked in the Added state.`. It does this for *every* class, so by definition is already generic. – Panagiotis Kanavos Dec 05 '22 at 18:25
  • Consider a case when I try to add an onion with open id that does not exist in my DB. An Update statement will throw an error, because Add operation was due. ```The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded``` – Nick Farsi Dec 05 '22 at 18:35
  • EF doesn't support real (atomic) upsert out of the box. If you don't need atomic then just check if entity exists first then decide yourself whether to insert or update (that approach might fail of course if parallel operation works on the same entity). – Evk Dec 09 '22 at 18:16
  • What is "open id" in your context? It's an ambiguous word. – Gert Arnold Dec 12 '22 at 11:58
  • An entity has an ID (PK) that has been assigned elsewhere. I want to use the same ID as PK in my DB – Nick Farsi Dec 12 '22 at 14:21
  • So all you mean is that they're not identity (or auto-increment) columns. Are you aware of how the Update method works? (See the comments above). – Gert Arnold Dec 13 '22 at 08:00
  • No, they are identity but they were assigned these elsewhere. Does not mean I can't use these ID in my DB as PKs – Nick Farsi Dec 13 '22 at 08:12

2 Answers2

0

Since you are wanting to do a 'BulkInsert' you can use the UpdateRange(IEnumerable items) method which is provided by the DbContext. So, in your case you would create a new method inside your repository and do:

public void Upsert<T>(IEnumerable<T> items)
{

    _context.DbSet<T>.UpdateRange(items);
    _context.SaveChanges(); // persist to db
}

then you can use it like this (pseudo code)

var myRepository = new repository<Onion>(_context);
var entities = _context.Onions;
foreach(var entity in entities){
   entity.Layers = 8
}
myRepository.Upsert(entities);
// you could do SaveChanges() here too.

If you want to do something custom, you can use the following, but it is not as efficient as UpdateRange because it does a query for each item in the collection passed in.

public void AddOrUpdateRange<T>(ICollection<T> items)
{
    foreach (var item in items)
    {
        var existingItem = _context.Set<T>().Find(item.Id);
        if (existingItem != null)
        {
            _context.Entry(existingItem).CurrentValues.SetValues(item);
        }
        else
        {
            _context.Set<T>().Add(item);
        }
    }
    _context.SaveChanges();
}
Jacob
  • 371
  • 2
  • 18
  • UpdateRange will give the following error if trying to update entries not present in DB ```The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded``` – Nick Farsi Dec 06 '22 at 18:57
  • the error indicates that you have not pulled the rows from the database. make sure you retrieve existing rows before trying to update it. – Jacob Dec 06 '22 at 19:45
  • it could also be related to: https://github.com/dotnet/efcore/issues/4073. – Jacob Dec 06 '22 at 19:50
  • I'm not pulling anything from database, I need to insert or update them. And make EF decide itself whether to add or update instead of checking manually. Hence the question – Nick Farsi Dec 06 '22 at 20:46
  • 1
    Well, to check if something exists in the first place you need to check query it first, right? – Jacob Dec 06 '22 at 20:57
  • You are using `Update` incorrectly then. You need to provide it with data to update or insert. Please see the following answer: https://stackoverflow.com/questions/53676084/entity-framework-core-database-operation-expected-to-affect-1-rows-but-actual – Jacob Dec 06 '22 at 21:21
-3

All good conversations/comments. I have a similar scenario of Upsert operation but i had to have my repo handle that. I tried to modify it to match your need and came up like below.

// Business layer
public async Task<bool> UpsertOnions(IEnumerable<Onions> list)
{
    var existingRecs = (await _onionRepo.Get(existing => list.Select(model => model.Pk).Contains(existing.Pk)).ToList();
    if (!existingRecs.Any())
        return await _onionRepo.UpdateRange(existingRecs);
    
    var allNetNew = list.Except(existingRecs);
    return await _onionRepo.AddRange(allNetNew);
}
// Repo implementation
public async Task<bool> UpdateRange(IEnumerable<T> list)
{
    _dbSet.UpdateRange(list);
    // manage to handle your own here like return values etc. 
    // call SaveChanges()
    return true; // change to your own implementation
}

public async Task<bool> AddRange(IEnumerable<T> list)
{
    _dbSet.AddRange(list);
    // manage to handle your own here like return values etc. 
    // call SaveChanges()
    return true; // change to your own implementation
}

Here, i go with my assumption that when you get your data and map to your desired data model, your external data with its own openId should get associated to the Pk field of the model.

I am also trying to speak in general aspect without specific Database provider in mind. Like, Postgres or SQL, where your datamodel would only decorate your Pk property as [Key].

Ak777
  • 346
  • 7
  • 18
  • According to the question the solution has to be generic – Nick Farsi Dec 10 '22 at 08:39
  • To what level are you expecting this to be genetic. As others clearly outlined, there should be some customization for your need. That could be achieved with generics implementation for your model as type T. And you may build a custom comparer using the property attr Key. – Ak777 Dec 11 '22 at 20:28
  • I assume that EF knows PK for every entity thus it can define if that PK is default then it should perform Add if it's not then Update. Thus T can be just any class – Nick Farsi Dec 11 '22 at 20:54
  • 1
    I wonder if you tested the result of `list.Except(existingRecs)`. Both lists don't contain the same instances. – Gert Arnold Dec 12 '22 at 11:42
  • Well that's an illustration of how he can implement based on the needs. May be, a custom comparer of IComparer may help to do the actual Except() per the OPs need. – Ak777 Dec 12 '22 at 21:51
  • It's an essential detail that should be mentioned. Now looking a bit closer it strikes me that nothing is actually updated since `existingRecs` come freshly from the database. You effectively discard all changes. Also, why don't you use the Update(Range) method only? Are you aware of how it works? – Gert Arnold Dec 13 '22 at 08:06