FlexLabs.Upsert: INSERT and UPDATE in one call across EF Core providers
FlexLabs.Upsert is a library that brings UPSERT functionality to common database providers for Entity Framework in their respective native SQL syntax
At a glance
- What is it?
- FlexLabs.Upsert adds an Upsert extension to Entity Framework Core that emits each database's own upsert syntax, PostgreSQL's ON CONFLICT DO UPDATE, MERGE for SQL Server and Oracle, and ON DUPLICATE KEY UPDATE for MySQL. The API is a fluent chain ending in RunAsync, and the interesting part is the extension point for providers it does not know.
- Who is it for?
- Adopt FlexLabs.Upsert when your Entity Framework Core model needs an insert-or-update in a single round trip and you would rather not hand-write provider specific SQL, especially for a counter style pattern where a matched row is incremented. Do not adopt it for bulk import, for a database whose upsert syntax the library does not cover without writing your own command runner, or when a plain update followed by an insert is fast enough for your volume.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 73 days ago.
- What is it written in?
- Mainly C#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Why an upsert primitive rather than two calls
FlexLabs.Upsert is a library that brings UPSERT functionality to common database providers for Entity Framework in their respective native SQL syntax. The README describes it as adding basic support for Upsert operations to EF Core.
The problem it solves is the classic one. Entity Framework Core's change tracker will insert a new entity and update an existing one, but deciding which of the two a given row is, without a read first, requires either a race-prone read-then-write or hand-written SQL. An upsert pushes that decision into the database, where the uniqueness constraint is already enforced, and it does it in one statement.
The pattern in the README's example is a counter. A DailyVisit record carries a user identifier, a date and a visit count, and the upsert ensures a row exists for that user and day, incrementing the count if the row was already there:
DataContext.DailyVisits
.Upsert(new DailyVisit
{
UserID = userID,
Date = DateTime.UtcNow.Date,
Visits = 1,
})
.On(v => new { v.UserID, v.Date })
.WhenMatched(v => new DailyVisit
{
Visits = v.Visits + 1,
})
.RunAsync();Read the chain from the top. Upsert takes the entity to write. On names the columns that identify a conflict, here the user and the date. WhenMatched describes what to do when a row already matches, and here the new value is expressed as the existing value plus one, which is a relative update rather than a replacement. RunAsync sends it. The whole operation is asynchronous to the end.
That relative update is the part that a plain entity update cannot express. Setting Visits to 1 in the object model would overwrite a count of 57 with 1, because the object does not know what is in the database. Here the increment happens in SQL.
Each provider gets its own dialect, generated by the library
The mechanism is stated in one line of the README and it is the core of the library. It uses INSERT ... ON CONFLICT DO UPDATE in PostgreSQL and SQLite, MERGE in SQL Server and Oracle, and INSERT INTO ... ON DUPLICATE KEY UPDATE in MySQL.
That is three different statement families, with different syntax, different parameter styles and different semantics for the matched branch, and the library hides that behind one fluent chain. The provider-specific SQL is not something the caller writes; the caller writes the entity, the key columns and the update expression, and the library renders the statement appropriate to the provider the DbContext is configured for.
The MySQL case shows why that matters. ON DUPLICATE KEY UPDATE does not take a conflict target at all, because MySQL resolves the conflict on any unique key. So the On clause that names the matching columns has no direct equivalent there, and the library has to decide what to do with it. Whatever it does, the difference is the kind of thing that is invisible until you point the same code at two databases.
The Oracle entry is worth noting too. Oracle is not a first class target of most EF Core providers, so the fact that MERGE appears next to SQL Server suggests the library is aimed at a specific set of enterprise backends rather than only at the open source ones. The exact behaviour of the matched branch in each dialect is not spelled out in the README, and the project points at its Usage wiki page for more examples, which is where that detail lives.
Adding a provider the library does not know
The README mentions one more capability in a single line, and it is the most extensible part of the design: the library also supports injecting SQL command runners to add support for other providers.
That is the escape hatch for a database outside the built-in list. Instead of the library deciding that PostgreSQL means ON CONFLICT DO UPDATE, you register a command runner that knows how to build that statement for your provider, and the fluent chain stays the same. The consequence is that the library's dialect knowledge is not a closed set, and a team with an unusual backend is not stuck with raw SQL at every call site.
It also means the abstraction is at the level of statement construction rather than at the level of change tracking. There is no attempt here to integrate with EF Core's own upsert support, no interceptor pipeline and no entity state manipulation. The library produces SQL for the provider and runs it, and the entity you pass in is a template for the row to write rather than a tracked entity in the change tracker.
That distinction is worth holding on to when you decide how to use it. Because the entity is a template, the update expression in the matched branch is evaluated in the database against the existing row, which is what makes the increment in the example work, and it also means the object you pass in will not be refreshed with the resulting database values unless you ask for it.
What the repository tells you about the build
The repository is a modern .NET solution and the layout is informative even though the README is short.
There is a FlexLabs.Upsert.slnx file, which is the newer solution format rather than the classic .sln, alongside Directory.Build.props and Directory.Packages.props. The central package management pattern, with a single Directory.Packages.props file pinning versions for the whole tree, is how the project keeps its dependencies consistent across the many packages that this library necessarily ships, since each database provider needs its own package.
The published package is FlexLabs.EntityFrameworkCore.Upsert on NuGet, and the badge shows the version tracked there. The src/ and test/ directories hold the code and its tests, certs/ holds signing material for the packages, and publish.sh and release.sh are the automation scripts. There is a .editorconfig and a .gitattributes, so the formatting and line ending rules are enforced in the repository rather than left to taste.
The release history shows the versioning scheme. The tags are named by branch, release/v10/10.1.0, release/v10/10.0.0 and release/v10/10.0.0-rc1, with 10.1.0 published on 2026-08-11, 10.0.0 on 2026-02-26 and the release candidate on 2026-02-03. A major version that tracks the Entity Framework Core generation is a sensible choice for a library that leans on provider internals, since those move with the framework.
The last push to the repository was on 2026-08-11 and the project is not archived, and the licence is MIT.
Where an upsert library is the wrong answer
The first limitation is bulk loading. An upsert is one statement per row shape, and the statement carries the values of a single entity. If the job is to import a million rows, an upsert per row is a network round trip per row, and whatever batching the provider offers is the right tool instead. The library is for the case where one logical write may or may not already exist, not for moving a table.
The second limitation is the conflict target. The On clause names columns, and the database has to have a unique index or constraint over exactly those columns for the conflict to be detected the way the author intended. If the unique index is on a different column set, the statement either conflicts on something else or conflicts on nothing, and the outcome differs between providers. This is the single most common way an upsert quietly does the wrong thing, and it is a schema question rather than a library question.
The third is that the matched branch semantics are not uniform. A relative expression like the increment in the example has a natural meaning in a statement that can see the existing row, but the exact rendering varies, and Oracle's MERGE in particular has its own rules about what may appear in the update clause. If your logic depends on a specific expression, check it against the provider you actually run.
The alternative is the one most teams reach for first: do the update and the insert yourself, in the right order, inside a transaction. That is more code and it needs care to avoid the race, but it uses only EF Core's own change tracker, it works on every provider including ones this library has never heard of, and its behaviour is completely transparent. The trade is the opposite of the usual one. Hand-written logic is portable and explicit but slow and repetitive. FlexLabs.Upsert is concise and provider aware, but it introduces a dependency on how each provider renders the statement, and on a schema decision about the unique index. For a handful of well understood upserts, the library removes real duplication. For a data pipeline that upserts constantly and in bulk, it is the wrong shape of tool.
Editorial conclusion
Adopt FlexLabs.Upsert when your Entity Framework Core model needs an insert-or-update in a single round trip and you would rather not hand-write provider specific SQL, especially for a counter style pattern where a matched row is incremented. Do not adopt it for bulk import, for a database whose upsert syntax the library does not cover without writing your own command runner, or when a plain update followed by an insert is fast enough for your volume. Before you commit, check that the provider you use is one of PostgreSQL, SQLite, SQL Server, Oracle or MySQL, read the Usage page in the wiki for the full set of clauses, and confirm that the key you pass to On is covered by a unique index in the database, since the conflict detection depends on it.
Frequently asked questions
What does FlexLabs.Upsert do?
It adds basic support for Upsert operations to Entity Framework Core, using INSERT ... ON CONFLICT DO UPDATE in PostgreSQL and SQLite, MERGE in SQL Server and Oracle, and INSERT INTO ... ON DUPLICATE KEY UPDATE in MySQL. The NuGet package is FlexLabs.EntityFrameworkCore.Upsert and the library is MIT licensed.
How do I write an upsert with FlexLabs.Upsert?
Call Upsert with the entity to write, name the conflict columns with On, describe the update for a matched row with WhenMatched, and finish with RunAsync. The example in the README inserts a DailyVisit and, when a row with the same user and date exists, increments its Visits value by one.
Can I use FlexLabs.Upsert with a database it does not support natively?
Yes. The library supports injecting SQL command runners to add support for other providers, so the statement construction for an unsupported backend can be supplied by the application while the fluent API stays the same.
Which Entity Framework Core version does FlexLabs.Upsert target?
The release tags are named by branch under release/v10, with 10.1.0 published on 2026-08-11, 10.0.0 on 2026-02-26 and a 10.0.0-rc1 on 2026-02-03. The last push to the repository was on 2026-08-11 and the repository is not archived.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/artiomchi-flexlabs-upsert)Was this useful?