How Does EF Core Handle Explicit Value for Generated Value Property? (2024)

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, we found out that it was way more complicated than what we were expecting when we had to research to fully understand the behavior to code our Explicit Value Resolution Mode option for our EF Extensions library.

In this article, we will share our 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 might be expected to be 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 the Customer entity type will be considered as an Key/Identity.
  • Data Annotations:
    • Concurrency Check
    • Database Generated
    • Timestamp
  • 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.

This section of your article effectively explains complex configurations and behaviors with a dash of humor to keep it engaging. I'll refine the text for clarity and correct minor grammatical inconsistencies to enhance readability:

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 TypeBehavior on InsertBehavior on Update
NoneAlways insertAlways update the value if you specify a different value
ComputedAlways ignore the valueAlways ignore the value
ConcurrencyAlways insert if an explicit value is provided, otherwise ignoreAlways update the value if you specify a different value
Default ValueAlways insert if an explicit value is provided, otherwise ignoreAlways update the value if you specify a different value
KeyAlways insert if an explicit value is provided, otherwise ignoreThrow an error if you specify a different value
IdentityAlways insert if an explicit value is provided, otherwise ignoreAlways update the value if you specify a different value
Row VersionAlways ignore the valueAlways 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:

MethodBehavior on InsertBehavior on Update
ValueGeneratedNeverAlways insertAlways update the value if you specify a different value
ValueGeneratedOnAddAlways insert if an explicit value is provided, otherwise ignoreAlways update the value if you specify a different value
ValueGeneratedOnAddOrUpdateAlways ignore the valueAlways ignore the value
ValueGeneratedOnUpdateAlways insertAlways ignore the value
ValueGeneratedOnUpdateSometimesAlways insertAlways 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 TypeBehavior on InsertBehavior on Update
NoneSaveSave
ComputedIgnoreIgnore
ConcurrencySaveSave
Default ValueSaveSave
KeySaveThrow
IdentitySaveSave
Row VersionIgnoreIgnore

And for ValueGenerated[XYZ] methods:

MethodBehavior on InsertBehavior on Update
ValueGeneratedNeverSaveSave
ValueGeneratedOnAddSaveSave
ValueGeneratedOnAddOrUpdateIgnoreIgnore
ValueGeneratedOnUpdateSaveIgnore
ValueGeneratedOnUpdateSometimesSaveSave

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)

language-csharp

|

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:

language-csharp

|

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:

language-exception-message

|

System.InvalidOperationException: The property 'GeneratedValueWithDataAnnotation.DefaultValue' is defined as read-only before it has been saved, but its value has been set to something other than a temporary or default value.

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 and AfterSaveBehavior.

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.

How Does EF Core Handle Explicit Value for Generated Value Property? (2024)
Top Articles
Latest Posts
Article information

Author: Mrs. Angelic Larkin

Last Updated:

Views: 5925

Rating: 4.7 / 5 (67 voted)

Reviews: 90% of readers found this page helpful

Author information

Name: Mrs. Angelic Larkin

Birthday: 1992-06-28

Address: Apt. 413 8275 Mueller Overpass, South Magnolia, IA 99527-6023

Phone: +6824704719725

Job: District Real-Estate Facilitator

Hobby: Letterboxing, Vacation, Poi, Homebrewing, Mountain biking, Slacklining, Cabaret

Introduction: My name is Mrs. Angelic Larkin, I am a cute, charming, funny, determined, inexpensive, joyous, cheerful person who loves writing and wants to share my knowledge and understanding with you.