Breaking changes included in EF Core 3.x
The following API and behavior changes have the potential to break existing applications when upgrading them to 3.x. Changes that we expect to only impact database providers are documented under provider changes.
Summary
High-impact changes
LINQ queries are no longer evaluated on the client
Tracking Issue #14935 Also see issue #12795
Old behavior
Before 3.0, when EF Core couldn't convert an expression that was part of a query to either SQL or a parameter, it automatically evaluated the expression on the client. By default, client evaluation of potentially expensive expressions only triggered a warning.
New behavior
Starting with 3.0, EF Core only allows expressions in the top-level projection (the last Select()
call in the query) to be evaluated on the client.
When expressions in any other part of the query can't be converted to either SQL or a parameter, an exception is thrown.
Why
Automatic client evaluation of queries allows many queries to be executed even if important parts of them can't be translated.
This behavior can result in unexpected and potentially damaging behavior that may only become evident in production.
For example, a condition in a Where()
call which can't be translated can cause all rows from the table to be transferred from the database server, and the filter to be applied on the client.
This situation can easily go undetected if the table contains only a few rows in development, but hit hard when the application moves to production, where the table may contain millions of rows.
Client evaluation warnings also proved too easy to ignore during development.
Besides this, automatic client evaluation can lead to issues in which improving query translation for specific expressions caused unintended breaking changes between releases.
Mitigations
If a query can't be fully translated, then either rewrite the query in a form that can be translated, or use AsEnumerable()
, ToList()
, or similar to explicitly bring data back to the client where it can then be further processed using LINQ-to-Objects.
Medium-impact changes
Entity Framework Core is no longer part of the ASP.NET Core shared framework
Tracking Issue Announcements#325
Old behavior
Before ASP.NET Core 3.0, when you added a package reference to Microsoft.AspNetCore.App
or Microsoft.AspNetCore.All
, it would include EF Core and some of the EF Core data providers like the SQL Server provider.
New behavior
Starting in 3.0, the ASP.NET Core shared framework doesn't include EF Core or any EF Core data providers.
Why
Before this change, getting EF Core required different steps depending on whether the application targeted ASP.NET Core and SQL Server or not. Also, upgrading ASP.NET Core forced the upgrade of EF Core and the SQL Server provider, which isn't always desirable.
With this change, the experience of getting EF Core is the same across all providers, supported .NET implementations and application types. Developers can also now control exactly when EF Core and EF Core data providers are upgraded.
Mitigations
To use EF Core in an ASP.NET Core 3.0 application or any other supported application, explicitly add a package reference to the EF Core database provider that your application will use.
The EF Core command-line tool, dotnet ef, is no longer part of the .NET Core SDK
Old behavior
Before 3.0, the dotnet ef
tool was included in the .NET Core SDK and was readily available to use from the command line from any project without requiring extra steps.
New behavior
Starting in 3.0, the .NET SDK does not include the dotnet ef
tool, so before you can use it you have to explicitly install it as a local or global tool.
Why
This change allows us to distribute and update dotnet ef
as a regular .NET CLI tool on NuGet, consistent with the fact that the EF Core 3.0 is also always distributed as a NuGet package.
Mitigations
To be able to manage migrations or scaffold a DbContext
, install dotnet-ef
as a global tool:
dotnet tool install --global dotnet-ef
You can also obtain it a local tool when you restore the dependencies of a project that declares it as a tooling dependency using a tool manifest file.
Low-impact changes
FromSql, ExecuteSql, and ExecuteSqlAsync have been renamed
Important
ExecuteSqlCommand
and ExecuteSqlCommandAsync
are deprecated. Use these methods instead.
Old behavior
Before EF Core 3.0, these method names were overloaded to work with either a normal string or a string that should be interpolated into SQL and parameters.
New behavior
Starting with EF Core 3.0, use FromSqlRaw
, ExecuteSqlRaw
, and ExecuteSqlRawAsync
to create a parameterized query where the parameters are passed separately from the query string.
For example:
context.Products.FromSqlRaw(
"SELECT * FROM Products WHERE Name = {0}",
product.Name);
Use FromSqlInterpolated
, ExecuteSqlInterpolated
, and ExecuteSqlInterpolatedAsync
to create a parameterized query where the parameters are passed as part of an interpolated query string.
For example:
context.Products.FromSqlInterpolated(
$"SELECT * FROM Products WHERE Name = {product.Name}");
Note that both of the queries above will produce the same parameterized SQL with the same SQL parameters.
Why
Method overloads like this make it very easy to accidentally call the raw string method when the intent was to call the interpolated string method, and the other way around. This could result in queries not being parameterized when they should have been.
Mitigations
Switch to use the new method names.
FromSql method when used with stored procedure cannot be composed
Old behavior
Before EF Core 3.0, FromSql method tried to detect if the passed SQL can be composed upon. It did client evaluation when the SQL was non-composable like a stored procedure. The following query worked by running the stored procedure on the server and doing FirstOrDefault on the client side.
context.Products.FromSqlRaw("[dbo].[Ten Most Expensive Products]").FirstOrDefault();
New behavior
Starting with EF Core 3.0, EF Core will not try to parse the SQL. So if you are composing after FromSqlRaw/FromSqlInterpolated, then EF Core will compose the SQL by causing sub query. So if you are using a stored procedure with composition then you will get an exception for invalid SQL syntax.
Why
EF Core 3.0 does not support automatic client evaluation, since it was error prone as explained here.
Mitigations
If you are using a stored procedure in FromSqlRaw/FromSqlInterpolated, you know that it cannot be composed upon, so you can add AsEnumerable
/AsAsyncEnumerable
right after the FromSql method call to avoid any composition on server side.
context.Products.FromSqlRaw("[dbo].[Ten Most Expensive Products]").AsEnumerable().FirstOrDefault();
FromSql methods can only be specified on query roots
Old behavior
Before EF Core 3.0, the FromSql
method could be specified anywhere in the query.
New behavior
Starting with EF Core 3.0, the new FromSqlRaw
and FromSqlInterpolated
methods (which replace FromSql
) can only be specified on query roots, i.e. directly on the DbSet<>
. Attempting to specify them anywhere else will result in a compilation error.
Why
Specifying FromSql
anywhere other than on a DbSet
had no added meaning or added value, and could cause ambiguity in certain scenarios.
Mitigations
FromSql
invocations should be moved to be directly on the DbSet
to which they apply.
No-tracking queries no longer perform identity resolution
Old behavior
Before EF Core 3.0, the same entity instance would be used for every occurrence of an entity with a given type and ID. This matches the behavior of tracking queries. For example, this query:
var results = context.Products.Include(e => e.Category).AsNoTracking().ToList();
would return the same Category
instance for each Product
that is associated with the given category.
New behavior
Starting with EF Core 3.0, different entity instances will be created when an entity with a given type and ID is encountered at different places in the returned graph. For example, the query above will now return a new Category
instance for each Product
even when two products are associated with the same category.
Why
Identity resolution (that is, determining that an entity has the same type and ID as a previously encountered entity) adds additional performance and memory overhead. This usually runs counter to why no-tracking queries are used in the first place. Also, while identity resolution can sometimes be useful, it is not needed if the entities are to be serialized and sent to a client, which is common for no-tracking queries.
Mitigations
Use a tracking query if identity resolution is required.
Temporary key values are no longer set onto entity instances
Old behavior
Before EF Core 3.0, temporary values were assigned to all key properties that would later have a real value generated by the database. Usually these temporary values were large negative numbers.
New behavior
Starting with 3.0, EF Core stores the temporary key value as part of the entity's tracking information, and leaves the key property itself unchanged.
Why
This change was made to prevent temporary key values from erroneously becoming permanent when an entity that has been previously tracked by some DbContext
instance is moved to a different DbContext
instance.
Mitigations
Applications that assign primary key values onto foreign keys to form associations between entities may depend on the old behavior if the primary keys are store-generated and belong to entities in the Added
state.
This can be avoided by:
- Not using store-generated keys.
- Setting navigation properties to form relationships instead of setting foreign key values.
- Obtain the actual temporary key values from the entity's tracking information.
For example,
context.Entry(blog).Property(e => e.Id).CurrentValue
will return the temporary value even thoughblog.Id
itself hasn't been set.
DetectChanges honors store-generated key values
Old behavior
Before EF Core 3.0, an untracked entity found by DetectChanges
would be tracked in the Added
state and inserted as a new row when SaveChanges
is called.
New behavior
Starting with EF Core 3.0, if an entity is using generated key values and some key value is set, then the entity will be tracked in the Modified
state.
This means that a row for the entity is assumed to exist and it will be updated when SaveChanges
is called.
If the key value isn't set, or if the entity type isn't using generated keys, then the new entity will still be tracked as Added
as in previous versions.
Why
This change was made to make it easier and more consistent to work with disconnected entity graphs while using store-generated keys.
Mitigations
This change can break an application if an entity type is configured to use generated keys but key values are explicitly set for new instances. The fix is to explicitly configure the key properties to not use generated values. For example, with the fluent API:
modelBuilder
.Entity<Blog>()
.Property(e => e.Id)
.ValueGeneratedNever();
Or with data annotations:
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public string Id { get; set; }
Cascade deletions now happen immediately by default
Old behavior
Before 3.0, EF Core applied cascading actions (deleting dependent entities when a required principal is deleted or when the relationship to a required principal is severed) did not happen until SaveChanges was called.
New behavior
Starting with 3.0, EF Core applies cascading actions as soon as the triggering condition is detected.
For example, calling context.Remove()
to delete a principal entity will result in all tracked related required dependents also being set to Deleted
immediately.
Why
This change was made to improve the experience for data binding and auditing scenarios where it is important to understand which entities will be deleted before SaveChanges
is called.
Mitigations
The previous behavior can be restored through settings on context.ChangeTracker
.
For example:
context.ChangeTracker.CascadeDeleteTiming = CascadeTiming.OnSaveChanges;
context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.OnSaveChanges;
Eager loading of related entities now happens in a single query
Old behavior
Before 3.0, eagerly loading collection navigations via Include
operators caused multiple queries to be generated on relational database, one for each related entity type.
New behavior
Starting with 3.0, EF Core generates a single query with JOINs on relational databases.
Why
Issuing multiple queries to implement a single LINQ query caused numerous issues, including negative performance as multiple database roundtrips were necessary, and data coherency issues as each query could observe a different state of the database.
Mitigations
While technically this is not a breaking change, it could have a considerable effect on application performance when a single query contains a large number of Include
operator on collection navigations. See this comment for more information and for rewriting queries in a more efficient way.
**
DeleteBehavior.Restrict has cleaner semantics
Old behavior
Before 3.0, DeleteBehavior.Restrict
created foreign keys in the database with Restrict
semantics, but also changed internal fixup in a non-obvious way.
New behavior
Starting with 3.0, DeleteBehavior.Restrict
ensures that foreign keys are created with Restrict
semantics--that is, no cascades; throw on constraint violation--without also impacting EF internal fixup.
Why
This change was made to improve the experience for using DeleteBehavior
in an intuitive manner, without unexpected side-effects.
Mitigations
The previous behavior can be restored by using DeleteBehavior.ClientNoAction
.
Query types are consolidated with entity types
Old behavior
Before EF Core 3.0, query types were a means to query data that doesn't define a primary key in a structured way. That is, a query type was used for mapping entity types without keys (more likely from a view, but possibly from a table) while a regular entity type was used when a key was available (more likely from a table, but possibly from a view).
New behavior
A query type now becomes just an entity type without a primary key. Keyless entity types have the same functionality as query types in previous versions.
Why
This change was made to reduce the confusion around the purpose of query types. Specifically, they are keyless entity types and they are inherently read-only because of this, but they should not be used just because an entity type needs to be read-only. Likewise, they are often mapped to views, but this is only because views often don't define keys.
Mitigations
The following parts of the API are now obsolete:
ModelBuilder.Query<>()
- InsteadModelBuilder.Entity<>().HasNoKey()
needs to be called to mark an entity type as having no keys. This would still not be configured by convention to avoid misconfiguration when a primary key is expected, but doesn't match the convention.DbQuery<>
- InsteadDbSet<>
should be used.DbContext.Query<>()
- InsteadDbContext.Set<>()
should be used.IQueryTypeConfiguration<TQuery>
- InsteadIEntityTypeConfiguration<TEntity>
should be used.
Note
Due to an issue in 3.x when querying keyless entities that have all properties set to null
a null
will be returned instead of an entity, if this issue is applicable to your scenario also add logic to handle null
in results.
Configuration API for owned type relationships has changed
Tracking Issue #12444 Tracking Issue #9148 Tracking Issue #14153
Old behavior
Before EF Core 3.0, configuration of the owned relationship was performed directly after the OwnsOne
or OwnsMany
call.
New behavior
Starting with EF Core 3.0, there is now fluent API to configure a navigation property to the owner using WithOwner()
.
For example:
modelBuilder.Entity<Order>.OwnsOne(e => e.Details).WithOwner(e => e.Order);
The configuration related to the relationship between owner and owned should now be chained after WithOwner()
similarly to how other relationships are configured.
While the configuration for the owned type itself would still be chained after OwnsOne()/OwnsMany()
.
For example:
modelBuilder.Entity<Order>.OwnsOne(e => e.Details, eb =>
{
eb.WithOwner()
.HasForeignKey(e => e.AlternateId)
.HasConstraintName("FK_OrderDetails");
eb.ToTable("OrderDetails");
eb.HasKey(e => e.AlternateId);
eb.HasIndex(e => e.Id);
eb.HasOne(e => e.Customer).WithOne();
eb.HasData(
new OrderDetails
{
AlternateId = 1,
Id = -1
});
});
Additionally calling Entity()
, HasOne()
, or Set()
with an owned type target will now throw an exception.
Why
This change was made to create a cleaner separation between configuring the owned type itself and the relationship to the owned type.
This in turn removes ambiguity and confusion around methods like HasForeignKey
.
Mitigations
Change configuration of owned type relationships to use the new API surface as shown in the example above.
Dependent entities sharing the table with the principal are now optional
Old behavior
Consider the following model:
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public OrderDetails Details { get; set; }
}
public class OrderDetails
{
public int Id { get; set; }
public string ShippingAddress { get; set; }
}
Before EF Core 3.0, if OrderDetails
is owned by Order
or explicitly mapped to the same table then an OrderDetails
instance was always required when adding a new Order
.
New behavior
Starting with 3.0, EF Core allows to add an Order
without an OrderDetails
and maps all of the OrderDetails
properties except the primary key to nullable columns.
When querying EF Core sets OrderDetails
to null
if any of its required properties doesn't have a value or if it has no required properties besides the primary key and all properties are null
.
Mitigations
If your model has a table sharing dependent with all optional columns, but the navigation pointing to it is not expected to be null
then the application should be modified to handle cases when the navigation is null
. If this is not possible a required property should be added to the entity type or at least one property should have a non-null
value assigned to it.
All entities sharing a table with a concurrency token column have to map it to a property
Old behavior
Consider the following model:
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public byte[] Version { get; set; }
public OrderDetails Details { get; set; }
}
public class OrderDetails
{
public int Id { get; set; }
public string ShippingAddress { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.Property(o => o.Version).IsRowVersion().HasColumnName("Version");
}
Before EF Core 3.0, if OrderDetails
is owned by Order
or explicitly mapped to the same table then updating just OrderDetails
will not update Version
value on client and the next update will fail.
New behavior
Starting with 3.0, EF Core propagates the new Version
value to Order
if it owns OrderDetails
. Otherwise an exception is thrown during model validation.
Why
This change was made to avoid a stale concurrency token value when only one of the entities mapped to the same table is updated.
Mitigations
All entities sharing the table have to include a property that is mapped to the concurrency token column. It's possible the create one in shadow-state:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<OrderDetails>()
.Property<byte[]>("Version").IsRowVersion().HasColumnName("Version");
}
Owned entities cannot be queried without the owner using a tracking query
Old behavior
Before EF Core 3.0, the owned entities could be queried as any other navigation.
context.People.Select(p => p.Address);
New behavior
Starting with 3.0, EF Core will throw if a tracking query projects an owned entity without the owner.
Why
Owned entities cannot be manipulated without the owner, so in the vast majority of cases querying them in this way is an error.
Mitigations
If the owned entity should be tracked to be modified in any way later then the owner should be included in the query.
Otherwise add an AsNoTracking()
call:
context.People.Select(p => p.Address).AsNoTracking();
Inherited properties from unmapped types are now mapped to a single column for all derived types
Old behavior
Consider the following model:
public abstract class EntityBase
{
public int Id { get; set; }
}
public abstract class OrderBase : EntityBase
{
public int ShippingAddress { get; set; }
}
public class BulkOrder : OrderBase
{
}
public class Order : OrderBase
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Ignore<OrderBase>();
modelBuilder.Entity<EntityBase>();
modelBuilder.Entity<BulkOrder>();
modelBuilder.Entity<Order>();
}
Before EF Core 3.0, the ShippingAddress
property would be mapped to separate columns for BulkOrder
and Order
by default.
New behavior
Starting with 3.0, EF Core only creates one column for ShippingAddress
.
Why
The old behavior was unexpected.
Mitigations
The property can still be explicitly mapped to separate column on the derived types:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Ignore<OrderBase>();
modelBuilder.Entity<EntityBase>();
modelBuilder.Entity<BulkOrder>()
.Property(o => o.ShippingAddress).HasColumnName("BulkShippingAddress");
modelBuilder.Entity<Order>()
.Property(o => o.ShippingAddress).HasColumnName("ShippingAddress");
}
The foreign key property convention no longer matches same name as the principal property
Old behavior
Consider the following model:
public class Customer
{
public int CustomerId { get; set; }
public ICollection<Order> Orders { get; set; }
}
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
}
Before EF Core 3.0, the CustomerId
property would be used for the foreign key by convention.
However, if Order
is an owned type, then this would also make CustomerId
the primary key and this isn't usually the expectation.
New behavior
Starting with 3.0, EF Core doesn't try to use properties for foreign keys by convention if they have the same name as the principal property. Principal type name concatenated with principal property name, and navigation name concatenated with principal property name patterns are still matched. For example:
public class Customer
{
public int Id { get; set; }
public ICollection<Order> Orders { get; set; }
}
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
}
public class Customer
{
public int Id { get; set; }
public ICollection<Order> Orders { get; set; }
}
public class Order
{
public int Id { get; set; }
public int BuyerId { get; set; }
public Customer Buyer { get; set; }
}
Why
This change was made to avoid erroneously defining a primary key property on the owned type.
Mitigations
If the property was intended to be the foreign key, and hence part of the primary key, then explicitly configure it as such.
Database connection is now closed if not used anymore before the TransactionScope has been completed
Old behavior
Before EF Core 3.0, if the context opens the connection inside a TransactionScope
, the connection remains open while the current TransactionScope
is active.
using (new TransactionScope())
{
using (AdventureWorks context = new AdventureWorks())
{
context.ProductCategories.Add(new ProductCategory());
context.SaveChanges();
// Old behavior: Connection is still open at this point
var categories = context.ProductCategories().ToList();
}
}
New behavior
Starting with 3.0, EF Core closes the connection as soon as it's done using it.
Why
This change allows to use multiple contexts in the same TransactionScope
. The new behavior also matches EF6.
Mitigations
If the connection needs to remain open explicit call to OpenConnection()
will ensure that EF Core doesn't close it prematurely:
using (new TransactionScope())
{
using (AdventureWorks context = new AdventureWorks())
{
context.Database.OpenConnection();
context.ProductCategories.Add(new ProductCategory());
context.SaveChanges();
var categories = context.ProductCategories().ToList();
context.Database.CloseConnection();
}
}
Each property uses independent in-memory integer key generation
Old behavior
Before EF Core 3.0, one shared value generator was used for all in-memory integer key properties.
New behavior
Starting with EF Core 3.0, each integer key property gets its own value generator when using the in-memory database. Also, if the database is deleted, then key generation is reset for all tables.
Why
This change was made to align in-memory key generation more closely to real database key generation and to improve the ability to isolate tests from each other when using the in-memory database.
Mitigations
This can break an application that is relying on specific in-memory key values to be set. Consider instead not relying on specific key values, or updating to match the new behavior.
Backing fields are used by default
Old behavior
Before 3.0, even if the backing field for a property was known, EF Core would still by default read and write the property value using the property getter and setter methods. The exception to this was query execution, where the backing field would be set directly if known.
New behavior
Starting with EF Core 3.0, if the backing field for a property is known, then EF Core will always read and write that property using the backing field. This could cause an application break if the application is relying on additional behavior coded into the getter or setter methods.
Why
This change was made to prevent EF Core from erroneously triggering business logic by default when performing database operations involving the entities.
Mitigations
The pre-3.0 behavior can be restored through configuration of the property access mode on ModelBuilder
.
For example:
modelBuilder.UsePropertyAccessMode(PropertyAccessMode.PreferFieldDuringConstruction);
Throw if multiple compatible backing fields are found
Old behavior
Before EF Core 3.0, if multiple fields matched the rules for finding the backing field of a property, then one field would be chosen based on some precedence order. This could cause the wrong field to be used in ambiguous cases.
New behavior
Starting with EF Core 3.0, if multiple fields are matched to the same property, then an exception is thrown.
Why
This change was made to avoid silently using one field over another when only one can be correct.
Mitigations
Properties with ambiguous backing fields must have the field to use specified explicitly. For example, using the fluent API:
modelBuilder
.Entity<Blog>()
.Property(e => e.Id)
.HasField("_id");
Field-only property names should match the field name
Old behavior
Before EF Core 3.0, a property could be specified by a string value and if no property with that name was found on the .NET type then EF Core would try to match it to a field using convention rules.
private class Blog
{
private int _id;
public string Name { get; set; }
}
modelBuilder
.Entity<Blog>()
.Property("Id");
New behavior
Starting with EF Core 3.0, a field-only property must match the field name exactly.
modelBuilder
.Entity<Blog>()
.Property("_id");
Why
This change was made to avoid using the same field for two properties named similarly, it also makes the matching rules for field-only properties the same as for properties mapped to CLR properties.
Mitigations
Field-only properties must be named the same as the field they are mapped to. In a future release of EF Core after 3.0, we plan to re-enable explicitly configuring a field name that is different from the property name (see issue #15307):
modelBuilder
.Entity<Blog>()
.Property("Id")
.HasField("_id");
AddDbContext/AddDbContextPool no longer call AddLogging and AddMemoryCache
Old behavior
Before EF Core 3.0, calling AddDbContext
or AddDbContextPool
would also register logging and memory caching services with DI through calls to AddLogging and AddMemoryCache.
New behavior
Starting with EF Core 3.0, AddDbContext
and AddDbContextPool
will no longer register these services with Dependency Injection (DI).
Why
EF Core 3.0 does not require that these services are in the application's DI container. However, if ILoggerFactory
is registered in the application's DI container, then it will still be used by EF Core.
Mitigations
If your application needs these services, then register them explicitly with the DI container using AddLogging or AddMemoryCache.
AddEntityFramework* adds IMemoryCache with a size limit
Old behavior
Before EF Core 3.0, calling AddEntityFramework*
methods would also register memory caching services with DI without a size limit.
New behavior
Starting with EF Core 3.0, AddEntityFramework*
will register an IMemoryCache service with a size limit. If any other services added afterwards depend on IMemoryCache they can quickly reach the default limit causing exceptions or degraded performance.
Why
Using IMemoryCache without a limit could result in uncontrolled memory usage if there is a bug in query caching logic or the queries are generated dynamically. Having a default limit mitigates a potential DoS attack.
Mitigations
In most cases calling AddEntityFramework*
is not necessary if AddDbContext
or AddDbContextPool
is called as well. Therefore, the best mitigation is to remove the AddEntityFramework*
call.
If your application needs these services, then register a IMemoryCache implementation explicitly with the DI container beforehand using AddMemoryCache.
DbContext.Entry now performs a local DetectChanges
Old behavior
Before EF Core 3.0, calling DbContext.Entry
would cause changes to be detected for all tracked entities.
This ensured that the state exposed in the EntityEntry
was up-to-date.
New behavior
Starting with EF Core 3.0, calling DbContext.Entry
will now only attempt to detect changes in the given entity and any tracked principal entities related to it.
This means that changes elsewhere may not have been detected by calling this method, which could have implications on application state.
Note that if ChangeTracker.AutoDetectChangesEnabled
is set to false
then even this local change detection will be disabled.
Other methods that cause change detection--for example ChangeTracker.Entries
and SaveChanges
--still cause a full DetectChanges
of all tracked entities.
Why
This change was made to improve the default performance of using context.Entry
.
Mitigations
Call ChangeTracker.DetectChanges()
explicitly before calling Entry
to ensure the pre-3.0 behavior.
String and byte array keys are not client-generated by default
Old behavior
Before EF Core 3.0, string
and byte[]
key properties could be used without explicitly setting a non-null value.
In such a case, the key value would be generated on the client as a GUID, serialized to bytes for byte[]
.
New behavior
Starting with EF Core 3.0 an exception will be thrown indicating that no key value has been set.
Why
This change was made because client-generated string
/byte[]
values generally aren't useful, and the default behavior made it hard to reason about generated key values in a common way.
Mitigations
The pre-3.0 behavior can be obtained by explicitly specifying that the key properties should use generated values if no other non-null value is set. For example, with the fluent API:
modelBuilder
.Entity<Blog>()
.Property(e => e.Id)
.ValueGeneratedOnAdd();
Or with data annotations:
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; }
ILoggerFactory is now a scoped service
Old behavior
Before EF Core 3.0, ILoggerFactory
was registered as a singleton service.
New behavior
Starting with EF Core 3.0, ILoggerFactory
is now registered as scoped.
Why
This change was made to allow association of a logger with a DbContext
instance, which enables other functionality and removes some cases of pathological behavior such as an explosion of internal service providers.
Mitigations
This change should not impact application code unless it is registering and using custom services on the EF Core internal service provider.
This isn't common.
In these cases, most things will still work, but any singleton service that was depending on ILoggerFactory
will need to be changed to obtain the ILoggerFactory
in a different way.
If you run into situations like this, please file an issue at on the EF Core GitHub issue tracker to let us know how you are using ILoggerFactory
such that we can better understand how not to break this again in the future.
Lazy-loading proxies no longer assume navigation properties are fully loaded
Old behavior
Before EF Core 3.0, once a DbContext
was disposed there was no way of knowing if a given navigation property on an entity obtained from that context was fully loaded or not.
Proxies would instead assume that a reference navigation is loaded if it has a non-null value, and that a collection navigation is loaded if it isn't empty.
In these cases, attempting to lazy-load would be a no-op.
New behavior
Starting with EF Core 3.0, proxies keep track of whether or not a navigation property is loaded. This means attempting to access a navigation property that is loaded after the context has been disposed will always be a no-op, even when the loaded navigation is empty or null. Conversely, attempting to access a navigation property that isn't loaded will throw an exception if the context is disposed even if the navigation property is a non-empty collection. If this situation arises, it means the application code is attempting to use lazy-loading at an invalid time, and the application should be changed to not do this.
Why
This change was made to make the behavior consistent and correct when attempting to lazy-load on a disposed DbContext
instance.
Mitigations
Update application code to not attempt lazy-loading with a disposed context, or configure this to be a no-op as described in the exception message.
Excessive creation of internal service providers is now an error by default
Old behavior
Before EF Core 3.0, a warning would be logged for an application creating a pathological number of internal service providers.
New behavior
Starting with EF Core 3.0, this warning is now considered and error and an exception is thrown.
Why
This change was made to drive better application code through exposing this pathological case more explicitly.
Mitigations
The most appropriate cause of action on encountering this error is to understand the root cause and stop creating so many internal service providers.
However, the error can be converted back to a warning (or ignored) via configuration on the DbContextOptionsBuilder
.
For example:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.ConfigureWarnings(w => w.Log(CoreEventId.ManyServiceProvidersCreatedWarning));
}
New behavior for HasOne/HasMany called with a single string
Old behavior
Before EF Core 3.0, code calling HasOne
or HasMany
with a single string was interpreted in a confusing way.
For example:
modelBuilder.Entity<Samurai>().HasOne("Entrance").WithOne();
The code looks like it is relating Samurai
to some other entity type using the Entrance
navigation property, which may be private.
In reality, this code attempts to create a relationship to some entity type called Entrance
with no navigation property.
New behavior
Starting with EF Core 3.0, the code above now does what it looked like it should have been doing before.
Why
The old behavior was very confusing, especially when reading the configuration code and looking for errors.
Mitigations
This will only break applications that are explicitly configuring relationships using strings for type names and without specifying the navigation property explicitly.
This is not common.
The previous behavior can be obtained through explicitly passing null
for the navigation property name.
For example:
modelBuilder.Entity<Samurai>().HasOne("Some.Entity.Type.Name", null).WithOne();
The return type for several async methods has been changed from Task to ValueTask
Old behavior
The following async methods previously returned a Task<T>
:
DbContext.FindAsync()
DbSet.FindAsync()
DbContext.AddAsync()
DbSet.AddAsync()
ValueGenerator.NextValueAsync()
(and deriving classes)
New behavior
The aforementioned methods now return a ValueTask<T>
over the same T
as before.
Why
This change reduces the number of heap allocations incurred when invoking these methods, improving general performance.
Mitigations
Applications simply awaiting the above APIs only need to be recompiled - no source changes are necessary.
A more complex usage (e.g. passing the returned Task
to Task.WhenAny()
) typically require that the returned ValueTask<T>
be converted to a Task<T>
by calling AsTask()
on it.
Note that this negates the allocation reduction that this change brings.
The Relational:TypeMapping annotation is now just TypeMapping
Old behavior
The annotation name for type mapping annotations was "Relational:TypeMapping".
New behavior
The annotation name for type mapping annotations is now "TypeMapping".
Why
Type mappings are now used for more than just relational database providers.
Mitigations
This will only break applications that access the type mapping directly as an annotation, which isn't common. The most appropriate action to fix is to use API surface to access type mappings rather than using the annotation directly.
ToTable on a derived type throws an exception
Old behavior
Before EF Core 3.0, ToTable()
called on a derived type would be ignored since only inheritance mapping strategy was TPH where this isn't valid.
New behavior
Starting with EF Core 3.0 and in preparation for adding TPT and TPC support in a later release, ToTable()
called on a derived type will now throw an exception to avoid an unexpected mapping change in the future.
Why
Currently it isn't valid to map a derived type to a different table. This change avoids breaking in the future when it becomes a valid thing to do.
Mitigations
Remove any attempts to map derived types to other tables.
ForSqlServerHasIndex replaced with HasIndex
Old behavior
Before EF Core 3.0, ForSqlServerHasIndex().ForSqlServerInclude()
provided a way to configure columns used with INCLUDE
.
New behavior
Starting with EF Core 3.0, using Include
on an index is now supported at the relational level.
Use HasIndex().ForSqlServerInclude()
.
Why
This change was made to consolidate the API for indexes with Include
into one place for all database providers.
Mitigations
Use the new API, as shown above.
Metadata API changes
New behavior
The following properties were converted to extension methods:
IEntityType.QueryFilter
->GetQueryFilter()
IEntityType.DefiningQuery
->GetDefiningQuery()
IProperty.IsShadowProperty
->IsShadowProperty()
IProperty.BeforeSaveBehavior
->GetBeforeSaveBehavior()
IProperty.AfterSaveBehavior
->GetAfterSaveBehavior()
Why
This change simplifies the implementation of the aforementioned interfaces.
Mitigations
Use the new extension methods.
Provider-specific Metadata API changes
New behavior
The provider-specific extension methods will be flattened out:
IProperty.Relational().ColumnName
->IProperty.GetColumnName()
IEntityType.SqlServer().IsMemoryOptimized
->IEntityType.IsMemoryOptimized()
PropertyBuilder.UseSqlServerIdentityColumn()
->PropertyBuilder.UseIdentityColumn()
Why
This change simplifies the implementation of the aforementioned extension methods.
Mitigations
Use the new extension methods.
EF Core no longer sends pragma for SQLite FK enforcement
Old behavior
Before EF Core 3.0, EF Core would send PRAGMA foreign_keys = 1
when a connection to SQLite is opened.
New behavior
Starting with EF Core 3.0, EF Core no longer sends PRAGMA foreign_keys = 1
when a connection to SQLite is opened.
Why
This change was made because EF Core uses SQLitePCLRaw.bundle_e_sqlite3
by default, which in turn means that FK enforcement is switched on by default and doesn't need to be explicitly enabled each time a connection is opened.
Mitigations
Foreign keys are enabled by default in SQLitePCLRaw.bundle_e_sqlite3, which is used by default for EF Core.
For other cases, foreign keys can be enabled by specifying Foreign Keys=True
in your connection string.
Microsoft.EntityFrameworkCore.Sqlite now depends on SQLitePCLRaw.bundle_e_sqlite3
Old behavior
Before EF Core 3.0, EF Core used SQLitePCLRaw.bundle_green
.
New behavior
Starting with EF Core 3.0, EF Core uses SQLitePCLRaw.bundle_e_sqlite3
.
Why
This change was made so that the version of SQLite used on iOS consistent with other platforms.
Mitigations
To use the native SQLite version on iOS, configure Microsoft.Data.Sqlite
to use a different SQLitePCLRaw
bundle.
Guid values are now stored as TEXT on SQLite
Old behavior
Guid values were previously stored as BLOB values on SQLite.
New behavior
Guid values are now stored as TEXT.
Why
The binary format of Guids is not standardized. Storing the values as TEXT makes the database more compatible with other technologies.
Mitigations
You can migrate existing databases to the new format by executing SQL like the following.
UPDATE MyTable
SET GuidColumn = hex(substr(GuidColumn, 4, 1)) ||
hex(substr(GuidColumn, 3, 1)) ||
hex(substr(GuidColumn, 2, 1)) ||
hex(substr(GuidColumn, 1, 1)) || '-' ||
hex(substr(GuidColumn, 6, 1)) ||
hex(substr(GuidColumn, 5, 1)) || '-' ||
hex(substr(GuidColumn, 8, 1)) ||
hex(substr(GuidColumn, 7, 1)) || '-' ||
hex(substr(GuidColumn, 9, 2)) || '-' ||
hex(substr(GuidColumn, 11, 6))
WHERE typeof(GuidColumn) == 'blob';
In EF Core, you could also continue using the previous behavior by configuring a value converter on these properties.
modelBuilder
.Entity<MyEntity>()
.Property(e => e.GuidProperty)
.HasConversion(
g => g.ToByteArray(),
b => new Guid(b));
Microsoft.Data.Sqlite remains capable of reading Guid values from both BLOB and TEXT columns; however, since the default format for parameters and constants has changed you'll likely need to take action for most scenarios involving Guids.
Char values are now stored as TEXT on SQLite
Old behavior
Char values were previously stored as INTEGER values on SQLite. For example, a char value of A was stored as the integer value 65.
New behavior
Char values are now stored as TEXT.
Why
Storing the values as TEXT is more natural and makes the database more compatible with other technologies.
Mitigations
You can migrate existing databases to the new format by executing SQL like the following.
UPDATE MyTable
SET CharColumn = char(CharColumn)
WHERE typeof(CharColumn) = 'integer';
In EF Core, you could also continue using the previous behavior by configuring a value converter on these properties.
modelBuilder
.Entity<MyEntity>()
.Property(e => e.CharProperty)
.HasConversion(
c => (long)c,
i => (char)i);
Microsoft.Data.Sqlite also remains capable of reading character values from both INTEGER and TEXT columns, so certain scenarios may not require any action.
Migration IDs are now generated using the invariant culture's calendar
Old behavior
Migration IDs were inadvertently generated using the current culture's calendar.
New behavior
Migration IDs are now always generated using the invariant culture's calendar (Gregorian).
Why
The order of migrations is important when updating the database or resolving merge conflicts. Using the invariant calendar avoids ordering issues that can result from team members having different system calendars.
Mitigations
This change affects anyone using a non-Gregorian calendar where the year is greater than the Gregorian calendar (like the Thai Buddhist calendar). Existing migration IDs will need to be updated so that new migrations are ordered after existing migrations.
The migration ID can be found in the Migration attribute in the migrations' designer files.
[DbContext(typeof(MyDbContext))]
-[Migration("25620318122820_MyMigration")]
+[Migration("20190318122820_MyMigration")]
partial class MyMigration
{
The Migrations history table also needs to be updated.
UPDATE __EFMigrationsHistory
SET MigrationId = CONCAT(LEFT(MigrationId, 4) - 543, SUBSTRING(MigrationId, 4, 150))
UseRowNumberForPaging has been removed
Old behavior
Before EF Core 3.0, UseRowNumberForPaging
could be used to generate SQL for paging that is compatible with SQL Server 2008.
New behavior
Starting with EF Core 3.0, EF will only generate SQL for paging that is only compatible with later SQL Server versions.
Why
We are making this change because SQL Server 2008 is no longer a supported product and updating this feature to work with the query changes made in EF Core 3.0 is significant work.
Mitigations
We recommend updating to a newer version of SQL Server, or using a higher compatibility level, so that the generated SQL is supported. That being said, if you are unable to do this, then please comment on the tracking issue with details. We may revisit this decision based on feedback.
Extension info/metadata has been removed from IDbContextOptionsExtension
Old behavior
IDbContextOptionsExtension
contained methods for providing metadata about the extension.
New behavior
These methods have been moved onto a new DbContextOptionsExtensionInfo
abstract base class, which is returned from a new IDbContextOptionsExtension.Info
property.
Why
Over the releases from 2.0 to 3.0 we needed to add to or change these methods several times. Breaking them out into a new abstract base class will make it easier to make these kind of changes without breaking existing extensions.
Mitigations
Update extensions to follow the new pattern.
Examples are found in the many implementations of IDbContextOptionsExtension
for different kinds of extensions in the EF Core source code.
LogQueryPossibleExceptionWithAggregateOperator has been renamed
Change
RelationalEventId.LogQueryPossibleExceptionWithAggregateOperator
has been renamed to RelationalEventId.LogQueryPossibleExceptionWithAggregateOperatorWarning
.
Why
Aligns the naming of this warning event with all other warning events.
Mitigations
Use the new name. (Note that the event ID number has not changed.)
Clarify API for foreign key constraint names
Old behavior
Before EF Core 3.0, foreign key constraint names were referred to as simply the "name". For example:
var constraintName = myForeignKey.Name;
New behavior
Starting with EF Core 3.0, foreign key constraint names are now referred to as the "constraint name". For example:
var constraintName = myForeignKey.ConstraintName;
Why
This change brings consistency to naming in this area, and also clarifies that this is the name of the foreign key constraint, and not the column or property name that the foreign key is defined on.
Mitigations
Use the new name.
IRelationalDatabaseCreator.HasTables/HasTablesAsync have been made public
Old behavior
Before EF Core 3.0, these methods were protected.
New behavior
Starting with EF Core 3.0, these methods are public.
Why
These methods are used by EF to determine if a database is created but empty. This can also be useful from outside EF when determining whether or not to apply migrations.
Mitigations
Change the accessibility of any overrides.
Microsoft.EntityFrameworkCore.Design is now a DevelopmentDependency package
Old behavior
Before EF Core 3.0, Microsoft.EntityFrameworkCore.Design was a regular NuGet package whose assembly could be referenced by projects that depended on it.
New behavior
Starting with EF Core 3.0, it is a DevelopmentDependency package. This means that the dependency won't flow transitively into other projects, and that you can no longer, by default, reference its assembly.
Why
This package is only intended to be used at design time. Deployed applications shouldn't reference it. Making the package a DevelopmentDependency reinforces this recommendation.
Mitigations
If you need to reference this package to override EF Core's design-time behavior, then you can update PackageReference item metadata in your project.
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.0.0">
<PrivateAssets>all</PrivateAssets>
<!-- Remove IncludeAssets to allow compiling against the assembly -->
<!--<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>-->
</PackageReference>
If the package is being referenced transitively via Microsoft.EntityFrameworkCore.Tools, you will need to add an explicit PackageReference to the package to change its metadata. Such an explicit reference must be added to any project where the types from the package are needed.
SQLitePCL.raw updated to version 2.0.0
Old behavior
Microsoft.EntityFrameworkCore.Sqlite previously depended on version 1.1.12 of SQLitePCL.raw.
New behavior
We've updated our package to depend on version 2.0.0.
Why
Version 2.0.0 of SQLitePCL.raw targets .NET Standard 2.0. It previously targeted .NET Standard 1.1 which required a large closure of transitive packages to work.
Mitigations
SQLitePCL.raw version 2.0.0 includes some breaking changes. See the release notes for details.
NetTopologySuite updated to version 2.0.0
Old behavior
The spatial packages previously depended on version 1.15.1 of NetTopologySuite.
New behavior
We've update our package to depend on version 2.0.0.
Why
Version 2.0.0 of NetTopologySuite aims to address several usability issues encountered by EF Core users.
Mitigations
NetTopologySuite version 2.0.0 includes some breaking changes. See the release notes for details.
Microsoft.Data.SqlClient is used instead of System.Data.SqlClient
Old behavior
Microsoft.EntityFrameworkCore.SqlServer previously depended on System.Data.SqlClient.
New behavior
We've updated our package to depend on Microsoft.Data.SqlClient.
Why
Microsoft.Data.SqlClient is the flagship data access driver for SQL Server going forward, and System.Data.SqlClient no longer be the focus of development. Some important features, such as Always Encrypted, are only available on Microsoft.Data.SqlClient.
Mitigations
If your code takes a direct dependency on System.Data.SqlClient, you must change it to reference Microsoft.Data.SqlClient instead; as the two packages maintain a very high degree of API compatibility, this should only be a simple package and namespace change.
Multiple ambiguous self-referencing relationships must be configured
Old behavior
An entity type with multiple self-referencing uni-directional navigation properties and matching FKs was incorrectly configured as a single relationship. For example:
public class User
{
public Guid Id { get; set; }
public User CreatedBy { get; set; }
public User UpdatedBy { get; set; }
public Guid CreatedById { get; set; }
public Guid? UpdatedById { get; set; }
}
New behavior
This scenario is now detected in model building and an exception is thrown indicating that the model is ambiguous.
Why
The resultant model was ambiguous and will likely usually be wrong for this case.
Mitigations
Use full configuration of the relationship. For example:
modelBuilder
.Entity<User>()
.HasOne(e => e.CreatedBy)
.WithMany();
modelBuilder
.Entity<User>()
.HasOne(e => e.UpdatedBy)
.WithMany();
DbFunction.Schema being null or empty string configures it to be in model's default schema
Old behavior
A DbFunction configured with schema as an empty string was treated as built-in function without a schema. For example following code will map DatePart
CLR function to DATEPART
built-in function on SqlServer.
[DbFunction("DATEPART", Schema = "")]
public static int? DatePart(string datePartArg, DateTime? date) => throw new Exception();
New behavior
All DbFunction mappings are considered to be mapped to user defined functions. Hence empty string value would put the function inside the default schema for the model. Which could be the schema configured explicitly via fluent API modelBuilder.HasDefaultSchema()
or dbo
otherwise.
Why
Previously schema being empty was a way to treat that function is built-in but that logic is only applicable for SqlServer where built-in functions do not belong to any schema.
Mitigations
Configure DbFunction's translation manually to map it to a built-in function.
modelBuilder
.HasDbFunction(typeof(MyContext).GetMethod(nameof(MyContext.DatePart)))
.HasTranslation(args => SqlFunctionExpression.Create("DatePart", args, typeof(int?), null));
EF Core 3.0 targets .NET Standard 2.1 rather than .NET Standard 2.0 Reverted
EF Core 3.0 targets .NET Standard 2.1, which is a breaking change which excludes .NET Framework applications. EF Core 3.1 reverted this and targets .NET Standard 2.0 again.
Query execution is logged at Debug level Reverted
We reverted this change because new configuration in EF Core 3.0 allows the log level for any event to be specified by the application. For example, to switch logging of SQL to Debug
, explicitly configure the level in OnConfiguring
or AddDbContext
:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(connectionString)
.ConfigureWarnings(c => c.Log((RelationalEventId.CommandExecuting, LogLevel.Debug)));