The first time I discovered that EF Core applies logic when I specify a value for a generated value property was by mistake.
I copied a test that had a value for the CustomerID
property, which is an identity column in my database, and called the SaveChanges
method.
To my surprise, I got the following error: Cannot insert explicit value for identity column in table 'MyTable' when IDENTITY_INSERT is set to OFF
. This error is common and easy to understand, but the first thing that came to my mind was, "Why the hell is SaveChanges
trying to insert into my identity column!"
The original test from which I copied didn't have this error. Instead, I was using the BulkSaveChanges method with the InsertKeepIdentity option. When this option is turned on, it means we specify to the method that we explicitly want to insert values into the identity columns, and the library already handles everything for me like turning on the IDENTITY_INSERT—so easy peasy.
But in this test, I was using the SaveChanges
method! So why was EF Core inserting into an identity column? And even more, if it does so, why then doesn't it turn on the IDENTITY_INSERT to allow it like Entity Framework Extensions is automatically doing for me?
I surely liked to ask myself a lot of questions that day!
So how exactly does EF Core handle explicit values? The answer, after days of tests and research, was very simple: EF Core will insert the value if one is provided, but not always; it will sometimes ignore the value provided, sometimes will insert even if no value is provided, and sometimes will throw an error. WAIT! What? Yeah, I found out that it was way more complicated than what I were expecting when I had to research to fully understand the behavior to code our Explicit Value Resolution Mode option for our EF Extensions library.
In this article, I will share my research, experience, and discoveries with you:
- How to Configure Generated Value?
- What is the Default Behavior for SaveChanges?
- The Truth About How EF Core Handles Generated Value Properties
- The Secret to Changing Generated Value Default Behavior with BeforeSaveBehavior / AfterSaveBehavior
After reading this article, you will master how EF Core works with explicit values for generated value properties and be able to code any behavior you will want to get.
How to Configure Generated Value?
Most of you probably already know how to configure common generated values, but let's do a quick reminder as the first step to understanding how EF Core handles explicit values for these properties is surely to first understand how to configure them.
A generated value is a property where the value is expected to be generated by either:
- EF Core itself (usually for
GUID
) - The database
The value is generated on insert, such as the case with an identity or default value column, and/or also generated on update, such as the case with a row version/timestamp column.
There are multiple ways to configure a generated value, and the most common are:
- Default Behavior
- For example, the
CustomerID
property for theCustomer
entity type will be considered as an Key/Identity.
- For example, the
- Data Annotations:
- Fluent API:
- HasComputedColumnSql
- HasDefaultValue
- HasDefaultValueSql
- IsConcurrencyToken
- IsRowVersion
- ValueGeneratedOnAdd
- ValueGeneratedOnAddOrUpdate
- ValueGeneratedOnUpdate
- ValueGeneratedOnUpdateSometimes
- UseIdentityColumn
Here are two online examples created on .NET Fiddle that show multiple different configurations of generated values and their behaviors:
Make sure to refer to these examples anytime you are unsure or want to explain explicit values to one of your colleagues.
These two online examples are very important as we will use them throughout our article.
If you run them, you will see that values are not always inserted or updated when you use SaveChanges
, even if a value is provided. This leads to the next section of this article.
What is the Default Behavior for SaveChanges?
Now that we've learned how to configure generated value properties, it's time to understand the default behavior of SaveChanges
.
For this, we created a table with common configuration types from our Online example using Fluent API:
Configuration Type | Behavior on Insert | Behavior on Update |
---|---|---|
None | Always insert | Always update the value if you specify a different value |
Computed | Always ignore the value | Always ignore the value |
Concurrency | Always insert if an explicit value is provided, otherwise ignore | Always update the value if you specify a different value |
Default Value | Always insert if an explicit value is provided, otherwise ignore | Always update the value if you specify a different value |
Key | Always insert if an explicit value is provided, otherwise ignore | Throw an error if you specify a different value |
Identity | Always insert if an explicit value is provided, otherwise ignore | Always update the value if you specify a different value |
Row Version | Always ignore the value | Always ignore the value |
Sometimes a property can have multiple configuration types, such as an identity which is also a key. Most of the time, you can assume the less permissive behavior. In this case, the behavior on update will be to throw an error instead of trying to update the value.
Here is the table when you configure your property with ValueGenerated[XYZ]
methods:
Method | Behavior on Insert | Behavior on Update |
---|---|---|
ValueGeneratedNever | Always insert | Always update the value if you specify a different value |
ValueGeneratedOnAdd | Always insert if an explicit value is provided, otherwise ignore | Always update the value if you specify a different value |
ValueGeneratedOnAddOrUpdate | Always ignore the value | Always ignore the value |
ValueGeneratedOnUpdate | Always insert | Always ignore the value |
ValueGeneratedOnUpdateSometimes | Always insert | Always update the value if you specify a different value |
One of the major sources of confusion comes from the methods ValueGeneratedOnAdd
and ValueGeneratedOnAddOrUpdate
. The ValueGeneratedOnAdd
method can sometimes insert a value if an explicit value is provided, while the ValueGeneratedOnAddOrUpdate
method will NEVER insert a value. This often adds a lot of confusion among Entity Framework developers, as at first glance, everyone expects them to act the same way when adding or inserting an entity.
Perhaps one day, this method will be renamed to ValueGeneratedOnAddSometimes
to be consistent with the ValueGeneratedOnUpdateSometimes
methods in terms of naming and behavior. But, since you've read this part of our article, you are now one step ahead of other developers—so it doesn't matter anymore, hehe.
At this stage, our table contains a lot of text and it's normal if things aren't 100% clear, but we will clarify this in the next section.
The Truth About How EF Core Handles Generated Value Properties
So, the configuration type or value generated method explains the explicit value behavior, right? WRONG! It’s the PropertySaveBehavior
state that dictates how explicit values are handled on insert and update.
It's now time to add some complexity to our article (finally!). Let’s first explore the PropertySaveBehavior enum and its three distinct states (Save, Ignore, Throw):
- Save:
- Insert (Without a Generated Value Property): EF Core will always insert the value.
- Insert (With a Generated Value Property): EF Core will insert the value provided, unless it’s considered the default value of the property type.
- Update: EF Core will update the column if a value different from the original is provided.
- Ignore:
- Insert: EF Core will always ignore the value, regardless of whether one is specified.
- Update: EF Core will always ignore the value, regardless of whether one is specified.
- Throw:
- Insert: EF Core will throw an error if a value is provided.
- Update: EF Core will throw an error if a value different from the original is provided.
Using the same table format as before, we can now observe the following behaviors:
Configuration Type | Behavior on Insert | Behavior on Update |
---|---|---|
None | Save | Save |
Computed | Ignore | Ignore |
Concurrency | Save | Save |
Default Value | Save | Save |
Key | Save | Throw |
Identity | Save | Save |
Row Version | Ignore | Ignore |
And for ValueGenerated[XYZ]
methods:
Method | Behavior on Insert | Behavior on Update |
---|---|---|
ValueGeneratedNever | Save | Save |
ValueGeneratedOnAdd | Save | Save |
ValueGeneratedOnAddOrUpdate | Ignore | Ignore |
ValueGeneratedOnUpdate | Save | Ignore |
ValueGeneratedOnUpdateSometimes | Save | Save |
The tables now start to be easier to read and understand if we refer to how the states for the PropertySaveBehavior
enum are handled.
And again, when we look at our two online examples (Data Annotations and Fluent API), we can see this exact behavior (obviously, since we double-checked this when creating this article!).
However, this table only shows the default behavior for SaveChanges
and not always the actual behavior, as this can easily be changed, as we will explore in the next section.
The Secret to Changing Generated Value Default Behavior with BeforeSaveBehavior / AfterSaveBehavior
In the previous section, we saw how the saving of a generated value completely depends on the PropertySaveBehavior
.
Which means we can change this PropertySaveBehavior
, right? RIGHT! Indeed, by choosing the value we want, we can get exactly the behavior we desire:
- BeforeSaveBehavior: Specifies how
SaveChanges
will act for this property when adding an entity (INSERT). - AfterSaveBehavior: Specifies how
SaveChanges
will act for this property when modifying an entity (UPDATE).
These values can be changed using the Entity Framework fluent API in the OnModelCreating
method with these two methods:
.Metadata.SetBeforeSaveBehavior(PropertySaveBehavior value)
.Metadata.SetAfterSaveBehavior(PropertySaveBehavior value)
modelBuilder.Entity<GeneratedValueWithDataAnnotation>()
.Property(x => x.DefaultValue)
.HasDefaultValue("Entity Framework Extensions")
.Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); // specifying an explicit value will now throw an error when adding an entity
You can retrieve the current behavior by using the GetBeforeSaveBehavior()
and GetAfterSaveBehavior()
methods. Here is a snippet that shows the current behavior for all properties of a specific entity type:
var entityType = context.Model.FindEntityType(typeof(EntitySimple));
foreach(var property in entityType.GetProperties())
{
Console.WriteLine($"{property.Name}: BeforeSaveBehavior = {property.GetBeforeSaveBehavior()}; AfterSaveBehavior = {property.GetAfterSaveBehavior()}");
}
Let's now examine a real-life scenario to better understand the purpose of this.
Suppose in my application I want to ensure that no other developer will EVER attempt to insert a value into a Default Value
column. All I have to do is set the BeforeSaveBehavior
to either Ignore
or Throw
. I choose Throw
as the error is very explicit.
Here is the online example with this configuration.
As seen in the example, if a developer tries to set an initial value to this property and runs the code... BOOM! They will immediately receive the following exception message:
This is exactly the behavior I was looking for!
Once you understand how BeforeSaveBehavior and AfterSaveBehavior work, you can now completely control how SaveChanges
acts when inserting or updating an entity. You can choose whether you want to allow explicit value, ignore it, or throw an error.
Conclusion
In this article, we have covered several important aspects:
- How to configure a generated value property.
- Understanding the default behavior of
SaveChanges
. - The crucial roles of
BeforeSaveBehavior
andAfterSaveBehavior
.
By now, you should be able to ensure that SaveChanges
behaves exactly as you want when inserting or updating an entity.
If your company is one of the thousands of lucky companies that use our Entity Framework Extensions library to enhance performance and gain more control over how your entities are saved, you'll likely be interested in learning how our Bulk Extensions handle default values. For more details, refer to our article How EFE Bulk Extensions Handle Explicit Values in EF Core.
If you feel something is missing from this article or have suggestions for improvement, please do not hesitate to contact us directly at info@zzzprojects.com and let us know what you would like us to add.