Issue
When I create my Account-class as example A, the object appears with state "EntityState.Modified" in my SaveChanges()-method as expected.
When I use example B, the object appears as "EntityState.Unchanged" and does no apear in the result of the GetObjectStateEntries(EntityState.Added | EntityState.Modified) call.
Can anyone explain to me, why the model object appears unchanged in example B while everything works in example A?
Thankyou
Classes
I update 'Created' and 'LastUpdated' using this method in my Context object:
public class CrmContext : DbContext
{
public override int SaveChanges()
{
DateTime now = DateTime.Now;
foreach (ObjectStateEntry entry in (this as IObjectContextAdapter).ObjectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Modified))
{
if (!entry.IsRelationship)
{
var account = entry.Entity as Account;
if (account != null)
{
account.LastUpdated = now;
if (entry.State == EntityState.Added)
{
account.Created = now;
}
}
}
}
return base.SaveChanges();
}
}
Model example A (WORKING):
public class Account
{
[Key]
public int AccountId { get; set; }
public string Name { get; set; }
[Timestamp]
public Byte[] TimeStamp { get; set; }
public DateTime Created { get; set; }
public DateTime LastUpdated { get; set; }
}
Model example B (NOT WORKING):
public class Account : BaseObject
{
[Key]
public int AccountId { get; set; }
public string Name { get; set; }
}
public class BaseObject
{
[Timestamp]
public Byte[] TimeStamp { get; set; }
public DateTime Created { get; set; }
public DateTime LastUpdated { get; set; }
}
UPDATE:
The problem does not seem to have anything to do with whether the model class inherits from a base class or not as I cannot reproduce a working example without using the suggested DetectChanges() call.
You should call change detection manually at the beginning of the overridden SaveChanges method:
public override int SaveChanges()
{
this.ChangeTracker.DetectChanges();
//...
}
I have no explanation why the entity state might be different for model A and model B. But the missing call to DetectChanges generally can be a reason why an entity is in state Unchanged (temporarily) although it has been modified. For example if you have POCO entities (not change tracking proxies), just load an entity, change a property and then call your overridden SaveChanges the state will be Unchanged until base.SaveChanges is called which would update the state to Modified and save the change to the database. But your code that sets the LastUpdated property would have been skipped and LastUpdated had the old value in the database.
Related
I have been working on a shop site project, using asp.net core spa templates provided with the latest VS2017, and have come across an issue that I haven't had before, possibly because until now my apps were quite simple!
I know what the problem is and where, I just can't fix it. I have a product model which has a collection of "Attributes" and a collection of "Variations" (different colour size, etc) and those variations also have attributes, so if the same Attribute shows up in the Variation (VAttributes), as is already in the main "Attributes" I get the error
InvalidOperationException: The instance of entity type
'ProductAttribute' cannot be tracked because another instance with the
key value 'Id:2' is already being tracked. When attaching existing
entities, ensure that only one entity instance with a given key value
is attached.
The best answer I found was here : https://stackoverflow.com/a/19695833/6749293
Unfortunately, even with the above check I got the error, I even tried making a list of attached attributes, and if the vattribute matched one of the items in the list, I didn't attach it. In fact I found that even if I don't attach (_context.attach()) any of the vAttributes, it still throws the error!.
Here's the code in question:
public async Task<Product> Create(Product product)
{
try
{
foreach (var variation in product.Variations)
{
foreach (var vAttr in variation.VAttributes)
{
bool isDetached = _context.Entry(vAttr).State == EntityState.Detached;
if (isDetached)
_context.Attach(vAttr);
}
}
foreach (var attribute in product.Attributes)
{
bool isDetached = _context.Entry(attribute).State == EntityState.Detached;
if (isDetached)
_context.Attach(attribute);
}
foreach (var category in product.Categories)
{
_context.Attach(category);
_context.Attach(category).Collection(x => x.Children);
}
_context.Products.Add(product);
await Save();
return product;
}
catch (Exception)
{
throw;
}
}
The models for the 3 objects are as follows:
public class Product
{
[Key, DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Description { get; set; }
public string StockRef { get; set; }
public DateTime? LastModified { get; set; }
//image needed
public ICollection<ProductCategory> Categories { get; set; }
public ICollection<ProductAttribute> Attributes { get; set; }
public ICollection<ProductVariation> Variations { get; set; }
public Product()
{
Attributes = new List<ProductAttribute>();
Variations = new List<ProductVariation>();
Categories = new List<ProductCategory>();
}
}
Variation:
public class ProductVariation
{
[Key, DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public DateTime? LastModified { get; set; }
public virtual ICollection<ProductAttribute> VAttributes { get; set; }
//needs images
public decimal VPrice { get; set; }
public string VStockRef { get; set; }
}
Finally the Attribute:
public class ProductAttribute
{
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public string Name { get; set; }
[ForeignKey("AttributeCategory")]
public int AttributeCategoryId { get; set; }
public virtual AttributeCategory AttributeCategory { get; set; }
}
Most help I found when searching was more related to having repo's injected as singletons, or HttpPut methods where the code had check for existence omitting the .AsNoTracking() or it was a mistake that they had the second instance in some way, where I am aware of the second instance, I just don't know how to prevent it from being tracked!
EDIT: I found that adding a foreign key on the ProductVariation model to the Product that was being created failed as it was only a temp key!? anyway removed it from the variation model, so have updated my code. Also thought I'd add one of my earler failed attempts, that led to all of the foreach loops.
_context.AttachRange(product.Attributes);
_context.AttachRange(product.Categories);
_context.AttachRange(product.Variations);
_context.Add(product);
I believe you can allow EF to handle the tracking.
public virtual bool Create(T item)
{
try
{
_context.Add(item);
_context.SaveChanges();
return true;
}
catch (Exception e)
{
return false;
}
}
This allows for you to save the entire object structure without worring about attaching items.
var newProduct = new Product();
newProduct.Categories.Add(cat);
newProduct.Attributes.Add(att);
newProduct.Variations.Add(vari);
Create(newProduct);
I want to insert current Datetime when ever a new row is inserted.
I am using Code First Approach of EF 6,MVC 5
After searching I got this
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime? CreatedDate { get; set; }
It has to be written in Models Class,but when ever a new record is inserted NULL is saved in DB,I have noticed that this is just declaration of CreatedDate,where to write CreatedDate = DateTime.Now
OR any other way to solve it.
My Entire Model
namespace December.Models
{
[Table("tblLibray")]
public class Library
{
[Key]
public int Id { get; set; }
[Required]
public string BookName { get; set; }
[Required]
public string Author { get; set; }
[Required]
public string Description { get; set; }
[Required]
public decimal MRP { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public DateTime? CreatedDate { get; set; }
}
}
The [DatabaseGenerated(DatabaseGeneratedOption.Computed)] attribute allows you to skip entity framework validation, but since your DateTime prop is nullable, you don't need it.
Create a new class
internal class AutoDateTimeMigrationSqlGenerator: SqlServerMigrationSqlGenerator //:NpgsqlMigrationSqlGenerator
{
protected override void Generate(AddColumnOperation addColumnOperation)
{
SetCreatedDateColumn(addColumnOperation.Column);
base.Generate(addColumnOperation);
}
protected override void Generate(CreateTableOperation createTableOperation)
{
SetCreatedDateColumn(createTableOperation.Columns);
base.Generate(createTableOperation);
}
private static void SetCreatedDateColumn(IEnumerable<ColumnModel> columns)
{
foreach (var columnModel in columns)
{
SetCreatedDateColumn(columnModel);
}
}
private static void SetCreatedDateColumn(PropertyModel column)
{
if (column.Name == "CreatedDate")
{
column.DefaultValueSql = "GETUTCDATE()";
}
}
}
In your Configuration.cs file (the one with the migration configuration class that inherits from DbMigrationsConfiguration<>) add the following line in the class constructor:
public Configuration()
{
AutomaticMigrationsEnabled = false;
SetSqlGenerator("System.Data.SqlClient", new AutoDateTimeMigrationSqlGenerator())
}
PS: Update-Database after all this.
I want to implement temporal properties using an approach similar to that described here, using Entity Framework code-first for database storage.
I want it optimized for getting the current value and have lazy loading for the history, but I don't want to have to add boilerplate code in the parent entity for every usage, as is the approach in the link above.
At the moment I have something like the code below, which by convention results in the database schema as shown below the code.
This will function as I need, but for performance reasons I'd like to avoid the join it requires to get the current property value (i.e. I want to move the TemporalStrings.CurrentValue DB column to Entities.Name instead).
If I try
modelBuilder.Entity<Entity>().Property(o => o.Name.CurrentValue).HasColumnName("Name");
it doesn't work. I get an exception like
The type 'ConsoleApplication1.TemporalString' has already been configured as an entity type. It cannot be reconfigured as a complex type.
Is there some way I can achieve this mapping, or is there a better approach for achieving this functionality?
Code:
public class TemporalString
{
public int Id { get; set; }
public string CurrentValue { get; set; } // Setter would be customized to append to History.
public virtual List<TemporalStringValue> History { get; set; }
// Other methods such as string ValueAt(DateTime) would exist.
}
public class TemporalStringValue
{
public int Id { get; set; }
public DateTime EffectiveFrom { get; set; }
public string Value { get; set; }
}
public class Entity
{
public int Id { get; set; }
public virtual TemporalString Name { get; set; }
}
public class TestDbContext : DbContext
{
public DbSet<Entity> Entities { get; set; }
public DbSet<TemporalString> TemporalStrings { get; set; }
public DbSet<TemporalStringValue> TemporalStringValues { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//modelBuilder.Entity<Entity>().Property(o => o.Name.CurrentValue).HasColumnName("Name");
// TODO: Map DB column TemporalStrings.CurrentValue to DB column Entities.Name?
}
}
internal class Program
{
private static void Main(string[] args)
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<TestDbContext>());
using (var context = new TestDbContext())
{
var entity = new Entity
{
Name = new TemporalString
{
CurrentValue = "Current Value",
History = new List<TemporalStringValue>
{
new TemporalStringValue
{
EffectiveFrom = DateTime.UtcNow,
Value = "Current Value"
},
new TemporalStringValue
{
EffectiveFrom = DateTime.UtcNow.AddMonths(-1),
Value = "Old Value"
},
new TemporalStringValue
{
EffectiveFrom = DateTime.UtcNow.AddMonths(-2),
Value = "Older Value"
}
}
}
};
context.Entities.Add(entity);
context.SaveChanges();
}
Console.Write("Done.");
Console.ReadKey();
}
}
Resulting schema:
Entities
(PK) Id
(FK) Name_Id (references TemporalStrings.Id)
TemporalStrings
(PK) Id
CurrentValue
TemporalStringValues
(PK) Id
EffectiveFrom
Value
(FK) TemporalString_Id
Desired schema:
Entities
(PK) Id
(FK) Name_Id (references TemporalStrings.Id)
Name (formerly TemporalStrings.CurrentValue)
TemporalStrings
(PK) Id
TemporalStringValues
(no change)
From what I understand on several posts the TPT architecure, with EF, does not create the necessary ON DELETE CASCADE when using a shared primary key.... It was also said that the EF context will handle the proper order of deletion of the sub-classed tables (however I do get an error that it breaks the constraint and that I can fix it with adding the ON DELETE CASCADE on the sub-class table)...
more background info...
I have a Section class, which has a number, title, and a list of pages. The page is designed using a super class which holds basic page properties. I have about 10+ sub-classes of the page class. The Section class holds an ICollection of these pages. The DB is created properly with the exception of no ON DELETE CASCADE on the sub-classed tables.
My code will create the entities and adds to the DB fine. However, if I try to delete a section (or all sections) it fails todelete due to the FK constraint on my sub-class page table...
public abstract BaseContent
{
... common properties which are Ignored in the DB ...
}
public class Course : BaseContent
{
public int Id {get;set;}
public string Name {get;set;}
public string Descripiton {get;set;}
public virtual ICollection<Chapter> Chapters{get;set;}
...
}
public class Chapter : BaseContent
{
public int Id {get;set;}
public int Number {get;set;}
public string Title {get;set;}
public virtual Course MyCourse{get;set;}
public virtual ICollection<Section> Sections{get;set;}
...
}
public class Section : BaseContent
{
public int Id {get;set;}
public int Number {get;set;}
public string Title {get;set;}
public virtual Chapter MyChapter {get;set;}
public virtual ICollection<BasePage> Pages {get;set;}
...
}
public abstract class BasePage : BaseContent, IComparable
{
public int Id { get; set; }
public string Title { get; set; }
public string PageImageRef { get; set; }
public ePageImageLocation ImageLocationOnPage { get; set; }
public int PageNumber { get; set; }
public virtual Section MySection { get; set; }
...
}
public class ChapterPage : BasePage
{
public virtual int ChapterNumber { get; set; }
public virtual string ChapterTitle { get; set; }
public virtual string AudioRef { get; set; }
}
public class SectionPage : BasePage
{
public virtual int SectionNumber { get; set; }
public virtual string SectionTitle { get; set; }
public virtual string SectionIntroduction { get; set; }
}
... plus about 8 other BasePage sub-classes...
public class MyContext: DbContext
{
...
public DbSet<Course> Courses { get; set; }
public DbSet<Chapter> Chapters { get; set; }
public DbSet<Section> Sections { get; set; }
public DbSet<BasePage> Pages { get; set; }
...
}
.. Fluent API ... (note Schema is defined to "" for SqlServer, for Oracle its the schema name)
private EntityTypeConfiguration<T> configureTablePerType<T>(string tableName) where T : BaseContent
{
var config = new EntityTypeConfiguration<T>();
config.ToTable(tableName, Schema);
// This adds the appropriate Ignore calls on config for the base class BaseContent
DataAccessUtilityClass.IgnoreAllBaseContentProperties<T>(config);
return config;
}
public virtual EntityTypeConfiguration<BasePage> ConfigurePageContent()
{
var config = configureTablePerType<BasePage>("PageContent");
config.HasKey(pg => pg.Id);
config.HasRequired(pg => pg.Title);
config.HasOptional(pg => pg.PageImageRef);
config.Ignore(pg => pg.ImageLocationOnPage);
return config;
}
public virtual EntityTypeConfiguration<ChapterPage> ConfigureChapterPage()
{
var config = configureTablePerType<ChapterPage>("ChapterPage");
config.HasOptional(pg => pg.AudioRef);
config.Ignore(pg => pg.ChapterNumber);
config.Ignore(pg => pg.ChapterTitle);
return config;
}
public virtual EntityTypeConfiguration<SectionPage> ConfigureSectionPage()
{
var config = configureTablePerType<SectionPage>("SectionPage");
config.HasOptional(pg => pg.AudioRef);
config.Ignore(pg => pg.SectionNumber);
config.Ignore(pg => pg.SectionTitle);
return config;
}
... other code to model other tables...
So the app is able to populate content and the relationships are properly set up. However, when I try to delete the course, I get the error that the delete failed due to the constraint on the ChapterPage to PageContent table..
Here is the code which deletes the Course (actually I delete all courses)...
using (MyContext ctx = new MyContext())
{
ctx.Courses.ToList().ForEach(crs => ctx.Courses.Remove(crs));
AttachLookupEntities(ctx);
ctx.SaveChanges();
}
If I add the 'ON DELETE CASCADE' in the ChapterPage and SectionPage table for its shared primary with PageContent, the delete goes through.
In summary,
The only solution that I have seen is to manually alter the constraints to add the ON DELETE CASCADE for all of my sub-class page tables. I can implement the change, as I have code which generates the DB script for the EF tables I need (a small subset of our whole DB) since we will not use EF to create or instantiate the DB (since it does not properly support migrations as yet...).
I sincerely hope that I have miscoded something, or forgot some setting in the model builder logic. Because if not, the EF designers have defined an architecure (TPT design approach) which cannot be used in any real world situation without a hack workaround. It's a half finished solution. Do not get me wrong, I like the work that has been done, and like most MSFT solutions its works for 70% of most basic application usages. It just is not ready for more complex situations.
I was trying to keep the DB design all within the EF fluent API and self-contained. It's about 98% there for me, just would be nice if they finished the job, maybe in the next release. At least it saves me all the CRUD operations.
Ciao!
Jim Shaw
I have reproduced the problem with a little bit simpler example:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Data.Entity;
namespace EFTPT
{
public class Parent
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<BasePage> Pages { get; set; }
}
public abstract class BasePage
{
public int Id { get; set; }
public string Name { get; set; }
public Parent Parent { get; set; }
}
public class DerivedPage : BasePage
{
public string DerivedName { get; set; }
}
public class MyContext : DbContext
{
public DbSet<Parent> Parents { get; set; }
public DbSet<BasePage> BasePages { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Parent>()
.HasMany(p => p.Pages)
.WithRequired(p => p.Parent); // creates casc. delete in DB
modelBuilder.Entity<BasePage>()
.ToTable("BasePages");
modelBuilder.Entity<DerivedPage>()
.ToTable("DerivedPages");
}
}
class Program
{
static void Main(string[] args)
{
using (var ctx = new MyContext())
{
var parent = new Parent { Pages = new List<BasePage>() };
var derivedPage = new DerivedPage();
parent.Pages.Add(derivedPage);
ctx.Parents.Add(parent);
ctx.SaveChanges();
}
using (var ctx = new MyContext())
{
var parent = ctx.Parents.FirstOrDefault();
ctx.Parents.Remove(parent);
ctx.SaveChanges(); // exception here
}
}
}
}
This gives the same exception that you had too. Only solutions seem to be:
Either setup cascading delete for the TPT constraint in the DB manually, as you already tested (or put an appropriate SQL command into the Seed method).
Or load the entites which are involved in the TPT inheritance into memory. In my example code:
var parent = ctx.Parents.Include(p => p.Pages).FirstOrDefault();
When the entities are loaded into the context, EF creates actually two DELETE statements - one for the base table and one for the derived table. In your case, this is a terrible solution because you had to load a much more complex object graph before you can get the TPT entities.
Even more problematic is if Parent has an ICollection<DerivedPage> (and the inverse Parent property is in DerivedPage then):
public class Parent
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<DerivedPage> Pages { get; set; }
}
public abstract class BasePage
{
public int Id { get; set; }
public string Name { get; set; }
}
public class DerivedPage : BasePage
{
public string DerivedName { get; set; }
public Parent Parent { get; set; }
}
The example code wouldn't throw an exception but instead delete the row from the derived table but not from the base table, leaving a phantom row which cannot represent an entity anymore because BasePage is abstract. This problem is not solvable by a cascading delete but you were actually forced to load the collection into the context before you can delete the parent to avoid such a nonsense in the database.
A similar question and analysis was here: http://social.msdn.microsoft.com/Forums/en-US/adodotnetentityframework/thread/3c27d761-4d0a-4704-85f3-8566fa37d14e/
It took me way too long to find a solution to the scenario described below. What should seemingly be a simple affair proved to be rather difficult. The question is:
Using Entity Framework 4.1 (Code First approach) and "Independent associations" how do I assign a different end to an existing "many to one" relationship in a "detached" scenario ( Asp.Net in my case).
The model:
I realize that using ForeignKey relationships instead of Independent Associations would have been an option, but it was my preference to not have a ForeignKey implementation in my Pocos.
A Customer has one or more Targets:
public class Customer:Person
{
public string Number { get; set; }
public string NameContactPerson { get; set; }
private ICollection<Target> _targets;
// Independent Association
public virtual ICollection<Target> Targets
{
get { return _targets ?? (_targets = new Collection<Target>()); }
set { _targets = value; }
}
}
A Target has one Customer:
public class Target:EntityBase
{
public string Name { get; set; }
public string Description { get; set; }
public string Note { get; set; }
public virtual Address Address { get; set; }
public virtual Customer Customer { get; set; }
}
Customer derives from a Person class:
public class Person:EntityBase
{
public string Salutation { get; set; }
public string Title { get; set; }
public string FirstName { get; set; }
public string LastName { get; set ; }
public string Telephone1 { get; set; }
public string Telephone2 { get; set; }
public string Email { get; set; }
public virtual Address Address { get; set; }
}
EntityBase class provides some common properties:
public abstract class EntityBase : INotifyPropertyChanged
{
public EntityBase()
{
CreateDate = DateTime.Now;
ChangeDate = CreateDate;
CreateUser = HttpContext.Current.User.Identity.Name;
ChangeUser = CreateUser;
PropertyChanged += EntityBase_PropertyChanged;
}
public void EntityBase_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (Id != new Guid())
{
ChangeDate = DateTime.Now;
ChangeUser = HttpContext.Current.User.Identity.Name;
}
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, e);
}
public event PropertyChangedEventHandler PropertyChanged;
public Guid Id { get; set; }
public DateTime CreateDate { get; set; }
public DateTime? ChangeDate { get; set; }
public string CreateUser { get; set; }
public string ChangeUser { get; set; }
}
The Context:
public class TgrDbContext : DbContext
{
public DbSet<Person> Persons { get; set; }
public DbSet<Address> Addresses { get; set; }
public DbSet<Customer> Customers { get; set; }
public DbSet<Target> Targets { get; set; }
public DbSet<ReportRequest> ReportRequests { get; set; }
// If OnModelCreating becomes to big, use "Model Configuration Classes"
//(derived from EntityTypeConfiguration) instead
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>().HasOptional(e => e.Address);
modelBuilder.Entity<Customer>().HasMany(c => c.Targets).WithRequired(t => t.Customer);
}
public static ObjectContext TgrObjectContext(TgrDbContext tgrDbContext)
{
return ((IObjectContextAdapter)tgrDbContext).ObjectContext;
}
}
I waited for #Martin answer because there are more solutions for this problem. Here is another one (at least it works with ObjectContext API so it should work with DbContext API as well):
// Existing customer
var customer = new Customer() { Id = customerId };
// Another existing customer
var customer2 = new Customer() { Id = customerId2 };
var target = new Target { ID = oldTargetId };
// Make connection between target and old customer
target.Customer = customer;
// Attach target with old customer
context.Targets.Attach(target);
// Attach second customer
context.Customers.Attach(customer2);
// Set customer to a new value on attached object (it will delete old relation and add new one)
target.Customer = customer2;
// Change target's state to Modified
context.Entry(target).State = EntityState.Modified;
context.SaveChanges();
The problem here is internal state model and state validations inside EF. Entity in unchanged or modified state with mandatory relation (on many side) cannot have independent association in added state when there is no other in deleted state. Modified state for association is not allowed at all.
There is a lot of information to be found on this topic; on stackoverflow I found Ladislav Mrnka's insights particularly helpful. More on the subject can also be found here: NTier Improvements for Entity Framework and here What's new in Entity Framework 4?
In my project (Asp.Net Webforms) the user has the option to replace the Customer assigned to a Target object with a different (existing) Customer object. This transaction is performed by a FormView control bound to an ObjectDataSource. The ObjectDataSource communicates with the BusinessLogic layer of the project which in turns passes the transaction to a repository class for the Target object in the DataAccess layer. The Update method for the Target object in the repository class looks like this:
public void UpdateTarget(Target target, Target origTarget)
{
try
{
// It is not possible to handle updating one to many relationships (i.e. assign a
// different Customer to a Target) with "Independent Associations" in Code First.
// (It is possible when using "ForeignKey Associations" instead of "Independent
// Associations" but this brings about a different set of problems.)
// In order to update one to many relationships formed by "Independent Associations"
// it is necessary to resort to using the ObjectContext class (derived from an
// instance of DbContext) and 'manually' update the relationship between Target and Customer.
// Get ObjectContext from DbContext - ((IObjectContextAdapter)tgrDbContext).ObjectContext;
ObjectContext tgrObjectContext = TgrDbContext.TgrObjectContext(_tgrDbContext);
// Attach the original origTarget and update it with the current values contained in target
// This does NOT update changes that occurred in an "Independent Association"; if target
// has a different Customer assigned than origTarget this will go unrecognized
tgrObjectContext.AttachTo("Targets", origTarget);
tgrObjectContext.ApplyCurrentValues("Targets", target);
// This will take care of changes in an "Independent Association". A Customer has many
// Targets but any Target has exactly one Customer. Therefore the order of the two
// ChangeRelationshipState statements is important: Delete has to occur first, otherwise
// Target would have temporarily two Customers assigned.
tgrObjectContext.ObjectStateManager.ChangeRelationshipState(
origTarget,
origTarget.Customer,
o => o.Customer,
EntityState.Deleted);
tgrObjectContext.ObjectStateManager.ChangeRelationshipState(
origTarget,
target.Customer,
o => o.Customer,
EntityState.Added);
// Commit
tgrObjectContext.Refresh(RefreshMode.ClientWins, origTarget);
tgrObjectContext.SaveChanges();
}
catch (Exception)
{
throw;
}
}
This works for the Update method for the Target object. Remarkably, the procedure for inserting a new Target object is way easier. DbContext recognizes the Customer end of the independent association properly and commits the change to the database without further ado. The Insert method in the repository class looks like this:
public void InsertTarget(Target target)
{
try
{
_tgrDbContext.Targets.Add(target);
_tgrDbContext.SaveChanges();
}
catch (Exception)
{
throw;
}
}
Hopefully this will be useful to somebody dealing with a similar task. If you notice a problem with this approach described above, please let me know in your comments. Thanks!