I have base class Book and and two sub classes EBook and PrintedBook. I have Fluent API and I use default inheritance strategy.
Database table schema:
- BookId (PK, int, not null)
- CategoryId (FK, int, not null)
- Title (nvarchar(100), not null)
- Added (datetime, not null)
- Url (nvarchar(max), null)
- Discriminator (nvarchar(128), not null)
I am updating one database record
- BookId: 42
- CategoryId: 1
- Title: "Whatever"
- Added: "2015-11-18 22:27:45.703"
- Url: NULL
- Discriminator: "PrintedBook"
using the change entity state this way:
EBook book2 = new EBook {BookId = 42, Title = "Tipy a triky pro iPhone 6", Added = DateTime.Now, CategoryId = 1, Url = "http://"};
context.Entry(book2).State = EntityState.Modified;
context.SaveChanges();
I expected that value for Discriminator will change, because I have type EBook.
SQL produced by EF
UPDATE [dbo].[Books]
SET [CategoryId] = 1 /* @0 */,
[Title] = 'Tipy a triky pro iPhone 6' /* @1 */,
[Added] = '2015-11-18T22:35:26' /* @2 */,
[Url] = 'http://' /* @3 */
WHERE ([BookId] = 42 /* @4 - [BookId] */)
Final state in DB
- BookId: 42
- CategoryId: 1
- Title: "Tipy a triky pro iPhone 6"
- Added: "2015-11-18 22:35:26.253"
- Url: "http://"
- Discriminator: "PrintedBook" // NOT CHANGED !!!
Entity Framework version 6.0.0.0
Hey,
EF does not support mutating between types in the inheritance hierarchy (since you can't do that in .NET code either). Because you are telling EF that book2 is an existing EBook it is just using the mapping for that class when it does the update.
~Rowan
@mholec can you try context.Entry(book2).Property("Discriminator).IsModified = true;
Notes for triage: This is possible, but you need to disable the IsReadOnlyAfterSave flag for the discriminator. The following listing successfully mutates the type.
``` c#
using Microsoft.Data.Entity;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
int bookId;
using (var db = new BookContext())
{
db.Database.EnsureCreated();
var book = new PrintedBook { Title = "Using EF7" };
db.Books.Add(book);
db.SaveChanges();
bookId = book.BookId;
}
using (var db = new BookContext())
{
var book = new EBook { BookId = bookId, Title = "Using EF7 E-Edition" };
db.Books.Update(book);
db.Entry(book).Property("Discriminator").CurrentValue = "EBook";
db.SaveChanges();
}
}
}
public abstract class Book
{
public int BookId { get; set; }
public string Title { get; set; }
}
public class EBook : Book
{
}
public class PrintedBook : Book
{
}
public class BookContext : DbContext
{
public DbSet<Book> Books { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Books;Trusted_Connection=True;");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PrintedBook>();
modelBuilder.Entity<EBook>();
modelBuilder.Entity<Book>().Property<string>("Discriminator").Metadata.IsReadOnlyAfterSave = false;
}
}
}
```
If you don't change the flag then you get the following exception
Unhandled Exception: System.InvalidOperationException: The property 'Discriminator' on entity type 'EBook' is defined to be read-only after it has been saved, but its value has been modified or marked as modifi
ed.
at Microsoft.Data.Entity.ChangeTracking.Internal.InternalEntityEntry.PrepareToSave()
at Microsoft.Data.Entity.ChangeTracking.Internal.StateManager.<>c.b__36_3(InternalEntityEntry e)
at System.Linq.Enumerable.WhereSelectEnumerableIterator2.MoveNext() at System.Collections.Generic.List1..ctor(IEnumerable1 collection) at System.Linq.Enumerable.ToList[TSource](IEnumerable1 source)
at Microsoft.Data.Entity.ChangeTracking.Internal.StateManager.GetEntriesToSave()
at Microsoft.Data.Entity.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Microsoft.Data.Entity.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Microsoft.Data.Entity.DbContext.SaveChanges()
at ConsoleApplication1.Program.Main(String[] args) in C:\Users\rowmil\Documents\Visual Studio 2015\Projects\ConsoleApplication1\ConsoleApplication1\Program.cs:line 30
And after changing the flag, if you try to mark as modified (rather than setting the current value), then you get the following exception.
Unhandled Exception: Microsoft.Data.Entity.DbUpdateException: An error occurred while updating the entries. See the inner exception for details. ---> System.Data.SqlClient.SqlException: Cannot insert the value
NULL into column 'Discriminator', table 'Books.dbo.Book'; column does not allow nulls. UPDATE fails.
The statement has been terminated.
at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action1 wrapCloseInAction) at System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action1 wrapCloseInAction)
at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
at System.Data.SqlClient.SqlDataReader.TryConsumeMetaData()
at System.Data.SqlClient.SqlDataReader.get_MetaData()
at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task& task, Boolean asyncWrite, SqlDataReader
ds, Boolean describeParameterEncryptionRequest)
at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, TaskCompletionSource1 completion, Int32 timeout, Task& task, B oolean asyncWrite) at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method) at System.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior behavior, String method) at System.Data.SqlClient.SqlCommand.ExecuteDbDataReader(CommandBehavior behavior) at System.Data.Common.DbCommand.ExecuteReader() at Microsoft.Data.Entity.Storage.Internal.RelationalCommand.Execute(IRelationalConnection connection, String executeMethod, Boolean openConnection, Boolean closeConnection, IReadOnlyDictionary2 parameterVal
ues)
at Microsoft.Data.Entity.Storage.Internal.RelationalCommand.ExecuteReader(IRelationalConnection connection, Boolean manageConnection, IReadOnlyDictionary2 parameterValues) at Microsoft.Data.Entity.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection) --- End of inner exception stack trace --- at Microsoft.Data.Entity.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection) at Microsoft.Data.Entity.Update.Internal.BatchExecutor.Execute(IEnumerable1 commandBatches, IRelationalConnection connection)
at Microsoft.Data.Entity.Storage.RelationalDatabase.SaveChanges(IReadOnlyList1 entries) at Microsoft.Data.Entity.ChangeTracking.Internal.StateManager.SaveChanges(IReadOnlyList1 entriesToSave)
at Microsoft.Data.Entity.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Microsoft.Data.Entity.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
at Microsoft.Data.Entity.DbContext.SaveChanges()
at ConsoleApplication1.Program.Main(String[] args) in C:\Users\rowmil\Documents\Visual Studio 2015\Projects\ConsoleApplication1\ConsoleApplication1\Program.cs:line 25
Press any key to continue . . .
We are ok with the rough edges for initial RTM since morphing between types isn't really a scenario we fully support.
Most helpful comment
Notes for triage: This is possible, but you need to disable the
IsReadOnlyAfterSaveflag for the discriminator. The following listing successfully mutates the type.``` c#
using Microsoft.Data.Entity;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
int bookId;
using (var db = new BookContext())
{
db.Database.EnsureCreated();
}
```
If you don't change the flag then you get the following exception
And after changing the flag, if you try to mark as modified (rather than setting the current value), then you get the following exception.