How to update the "LastModifiedDate" timestamp automatically on parent entity when adding/removing child entities - entity-framework

Is there a way to automatically enforce parent entity to be timestamped as having been modified, if any of its dependent child items are added/deleted/modified? The key word is automatically. I know this can be done by manipulating the DbEntry's EntityState or by manually setting the timestamp field in the parent, but I need this done on a number of parent-child entities in a system, so the desire is to have EF (or a related component) automatically do this somehow.
More Background and Examples
Let's say we have an Order and Order Items (1-many). When order items are added/removed from an order, the parent order itself needs to be updated to store the last modified timestamp.
public interface IModifiableEntity
{
DateTime LastModifiedOn { get; set; }
}
public class Order : IModifiableEntity
{
// some Order fields here...
// timestamp for tracking when the order was changed
public DateTime LastModifiedOn { get; set; }
// list of order items in a child collection
public ICollection<OrderItem> OrderItems { get; set; }
}
public class OrderItem
{
public int OrderId { get; set; }
// other order item fields...
}
Somewhere in application logic:
public void AddOrderItem(OrderItem orderItem)
{
var order = _myDb.Orders.Single(o => o.Id == orderItem.OrderId);
order.OrderItems.Add(orderItem);
_myDb.SaveChanges();
}
I already have a pattern in place to detect modified entities and set timestamps automatically via EF's SaveChanges, like this:
public override int SaveChanges()
{
var timestamp = DateTime.Now;
foreach (var modifiableEntity in ChangeTracker.Entries<IModifiableEntity>())
{
if (modifiableEntity.State == EntityState.Modified)
{
modifiableEntity.Entity.UpdatedOn = timestamp;
}
}
return base.SaveChanges();
}
That works great if any direct fields on an IModifiableEntity are updated. That entity's state will then be marked as Modified by EF, and my custom SaveChanges() above will catch it and set the timestamp field correctly.
The problem is, if you only interact with a child collection property, the parent entity is not marked as modified by EF. I know I can manually force that via context.Entry(myEntity).State or just by manually setting the LastModifiedOn field when adding child items in application logic, but that wouldn't be done centrally, and is easy to forget.
I DO NOT want to do this:
public void AddOrderItem(OrderItem orderItem)
{
var order = _myDb.Orders.Single(o => o.Id == orderItem.OrderId);
order.OrderItems.Add(orderItem);
// this works but is very manual and EF infrastructure specific
_myDb.Entry(order).State = EntityState.Modified;
// this also works but is very manual and easy to forget
order.LastModifiedOn = DateTime.Now;
_myDb.SaveChanges();
}
Any way I can do this centrally and inform EF that a "root" entity of a parent-child relationship needs to be marked as having been updated?

Related

Cannot insert explicit value for identity column in table when IDENTITY_INSERT is set to OFF - EF Core many-to-many (child) data

Following through Julie Lerman's Pluralsight course EF Core 6 Fundamentals I've created two classes in my own project (my own design, but identical to the course in terms of class structure/data hierarchy):
Class 1: Events - To hold information about an event being held (e.g. a training course), with a title and description (some fields removed for brevity):
public class EventItem
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int EventItemId { get; set; }
[Required(AllowEmptyStrings = false)]
public string EventTitle { get; set; }
public string? EventDescription { get; set; }
[Required]
public List<EventCategory> EventCategories { get; set; } = new();
}
Class 2: Event categories - Each event can be linked to one or more pre-existing (seeded) categories (e.g. kids, adult).
public class EventCategory
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int EventCategoryId { get; set; }
[Required]
public string EventCategoryName { get; set; }
public List<EventItem>? EventItems { get; set; }
}
In my Razor form to create the event, the user can select from multiple categories. Using EF Core I take the posted data (via a VM/DTO object) and construct the relevant parent/child entities. However upon saving to the database I get an exception as EF Core tries to re-create the categories when they already exist:
Cannot insert explicit value for identity column in table
'EventCategories' when IDENTITY_INSERT is set to OFF.
My code explicitly looks up the existing categories selected by the user, but the context tracker appears to still believe they need inserting, in addition to creating the many-to-many relationship.
I'd appreciate any input as to why this is happening please:
using (var dbcontext = DbFactory.CreateDbContext())
{
// Get selected categories from user's check box list
var selectedCategoryIds = _eventCagetories.Where(c => c.isSelected).Select(c => c.EventCategoryId).ToList();
// Create new Event
var newEventItem = new EventFinderDomain.Models.EventItem() {
EventTitle = _eventItemDTO.EventTitle,
EventDescription = _eventItemDTO.EventDescription,
EventUrl = _eventItemDTO.EventUrl,
TicketUrl = _eventItemDTO.TicketUrl
};
// Find categories from the database based on their ID value
var selectedEventCategories = dbcontext.EventCategories.Where(c => selectedCategoryIds.Contains(c.EventCategoryId)).ToList();
// Add the categories to the event
newEventItem.EventCategories!.AddRange(selectedEventCategories);
// Add the event to the change tracker
await dbcontext.EventItems.AddAsync(newEventItem); // <-- Created correctly with child list objects added
// Detect changes for debugging
dbcontext.ChangeTracker.DetectChanges();
var debugView = dbcontext.ChangeTracker.DebugView; // <-- Incorrectly shows newEventItem.Categories being added
// Save to database
await dbcontext.SaveChangesAsync(); // <-- Cannot insert explicit value for identity column
}
The Event entity appears to be correctly created in the debugger with its related child categories included:
The change tracker however incorrectly shows the selected categories being added again when they already exist:
After commenting out every line of code in the app and adding back in until it broke, it emerges the problem was elsewhere within Program.cs:
builder.Services.AddDbContextFactory<EventFinderContext>(
opt => opt.UseSqlServer(new SqlConnectionStringBuilder() {/*...*/}.ConnectionString)
.EnableSensitiveDataLogging()
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) // <-- THE CULPRIT
);
In the training video this method was described as a way of reducing overhead for disconnected apps. I had assumed that because of the disconnected nature of HTTP, this would be beneficial and that context would be re-established when creating the model's child data. This was incorrect on my part.
I should have used .AsNoTracking() only when retriving read-only data from my database. For example, loading in the child-data for a new model that wouldn't be modified directly, but used to create the many-to-many data (explicitly, for the category data option items only and not for the event data).

Adding a new entity to collection in attached entity causes ConcurrencyException

I have simplified the code below to show the root of the problem. My real code is using GenericRepository and UnitOfWork pattern but I get the same exception with this simplified code too.
I am using Entity Framework 6, Code First
It uses the following POCO entities
public class Order
{
public int Id {get;set;}
public virtual List<OrderProducts> OrderProducts {get;set;}
...
}
public class Product
{
public int Id {get;set;}
...
}
public class OrderProduct
{
public int OrderId {get;set;}
public int ProductId {get;set;}
public int Quantity
public virtual Order Order { get; set; }
public virtual Product Product{ get; set; }
}
The user is able to create a new product and add it to the order products on the same screen.
//Pull an order from the database:
var existingOrder = db.Orders.FirstOrDefault(x => x.Id == inputModel.OrderId);
//Iterate the OrderProductInputModels (IMs) in the Inputmodel
foreach (var orderProductIM in inputModel.OrderProductIMs )
{
var orderProduct = existingOrder.OrderProducts.SingleOrDefault(o => o.Id == orderProductIM.Id);
//if its an existing order product (already in db)
if (orderProduct != null)
{
//just update its property values
}
//if it has been added
else
{
//we need to create a new product first
var newProduct= new Product() { <set some properties> };
orderProduct= new OrderProduct()
{
Product=newProduct,
Order=existingOrder
}
//Add the OrderProduct to the order
existingOrder.OrderProducts.Add(orderProduct);
}
db.SaveChanges();
On save changes, I get the following error.
[System.Data.Entity.Infrastructure.DbUpdateConcurrencyException] = {"Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. Refresh ObjectStateManager entries."}
Why is this?
I expected entity framework to see that the existingOrders nested properties were newly added and unattached, update the order and create the new OrderProduct and Product.
Should it not be other way around in your if clause as you are checking for null ( then only it is a new order product else update. Issue is here:
//if its an existing order product (already in db)
if (orderProduct == null)
{
//just update its property values
}
//if it has been added
else
{
When you are looping around all the OrderProducts, you are constantly updating the database but the existingOrder object is not getting refreshed. Update that or add all the objects first and then update the database.
Finally solved it by creating a test project and reverse code first engineering the database. Noticed that OrderProduct entity was not generated. On inspecting the database, the primary key was not set. Once I set the primary key in the database, the issue was resolved. Thanks for all the suggestions.

Many to Many Relationships not saving

I have two entities with a fairly standard Many to Many relationship that I created in EF 5 Code First. These are Service and ServiceItem. The Service entity contains a collection of ServiceItems and the ServiceItem contains a collection of Services. I can create, change and save data to either of the entities basic properties with no problems. When I try to add a ServiceItem to a Service or a Service to a ServiceItem it seems to work, but nothing is saved. I have verified that all the proper database tables are created, including a ServiceItemService table with the cross keys. The database ServiceItemService table doesn't get any entry when I add the items. There is no error and everything else seems to work perfectly.
I am a bit stumped and could use some help. Below are the classes.
The Service class;
public class Service
{
//Default constructor
public Service()
{
//Defaults
IsActive = true;
ServicePeriod = ServicePeriodType.Monthly;
ServicePeriodDays = 0;
ServiceItems = new Collection<ServiceItem>();
}
public int ServiceID { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public ICollection<ServiceItem> ServiceItems { get; set; }
public string TermsOfService { get; set; }
public ServicePeriodType ServicePeriod { get; set; }
public int ServicePeriodDays { get; set; }
public bool IsActive { get; set; }
}
The ServiceItem class;
public class ServiceItem
{
public ServiceItem()
{
IsActive = true;
}
public int ServiceItemID { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public ICollection<Service> Services { get; set; }
public string UserRole { get; set; }
public bool IsActive { get; set; }
}
This is the Fluent mapping I did while trying to debug this issue. The same problem happened before and after adding this mapping.
public DbSet<Service> Services { get; set; }
public DbSet<ServiceItem> ServiceItems { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Service>()
.HasMany(p => p.ServiceItems)
.WithMany(r => r.Services)
.Map(mc =>
{
mc.MapLeftKey("ServiceItemID");
mc.MapRightKey("ServiceID");
mc.ToTable("ServiceItemService");
});
}
Here is the code I use to save the Service item that includes 2-3 ServiceItems in the Service.ServiceItems collection. I have carefully verified that the ServiceItems were in the proper collection.
db.Entry(dbService).State = EntityState.Modified;
db.SaveChanges();
The dbService object doesn't seem to get affected in any way. The ServiceItems are still in the proper collection, but no update are made to the ServiceItemService database table. Any advice would be very welcome.
-Thanks
It is expected that nothing happens.
What you want to change or add is a relationship between the entities Service and ServiceItem. But you cannot manipulate relationships by setting the state of an entity to Modified. This only updates scalar and complex properties but no navigation properties (= relationships). (For example setting the state of a Service entity to Modified will mark Service.Title and Service.Description, etc. as modified and ensure that those properties are saved to the database. But it doesn't care about the content of Service.ServiceItems.)
The only exception where you can change a relationship by setting the state to Modified are Foreign Key Associations. These are associations that have foreign key properties exposed in your model entity and they can only occur for one-to-many or one-to-one associations. Many-to-many relationships are always Independent Associations which means they can never have a foreign key property in an entity. (Because the FKs are in the join table, but the join table is not an entity and "hidden" from your model classes.)
There is a way to directly manipulate relationships for a many-to-many association but it requires to go down to the ObjectContext and its RelationshipManager which is - in my opinion - pretty advanced and tricky.
The usual and straight-forward way to add and remove relationship entries to/from a many-to-many association is by just adding items to and removing items from the collections while the entities are attached to the context. EF's change tracking mechanism will recognize the changes you have done and generate the appropriate INSERT, UPDATE and DELETE statements when you call SaveChanges.
The exact procedure depends on if you also want to save Service and/or ServiceItem as new entities or if you only want to add relationships between existing entities. Here are a few examples:
service should be INSERTed, all serviceItems should be INSERTed and the relationships between the entities should be INSERTed into the join table as well:
using (var context = new MyContext())
{
var service = new Service();
var serviceItem1 = new ServiceItem();
var serviceItem2 = new ServiceItem();
service.ServiceItems.Add(serviceItem1);
service.ServiceItems.Add(serviceItem2);
context.Services.Add(service);
context.SaveChanges();
}
Adding the "root" service of the object graph is enough because EF will recognize that all other entities in the graph are not attached to the context and assume that they have to be INSERTed into the database.
service already exists and should NOT be INSERTed, all serviceItems should be INSERTed and the relationships between the entities should be INSERTed into the join table as well:
using (var context = new MyContext())
{
var service = new Service { ServiceID = 15 };
context.Services.Attach(service);
var serviceItem1 = new ServiceItem();
var serviceItem2 = new ServiceItem();
service.ServiceItems.Add(serviceItem1);
service.ServiceItems.Add(serviceItem2);
context.SaveChanges();
}
EF recognizes here (when SaveChanges is called) that service is attached but the other entities are not. No INSERT for service happens but the serviceItem1/2 will be INSERTed together with the relationship entries.
service already exists and should NOT be INSERTed, all serviceItems already exist and should NOT be INSERTed, but the relationships between the entities should be INSERTed into the join table:
using (var context = new MyContext())
{
var service = new Service { ServiceID = 15 };
context.Services.Attach(service);
var serviceItem1 = new ServiceItem { ServiceItemID = 23 };
context.ServiceItems.Attach(serviceItem1);
var serviceItem2 = new ServiceItem { ServiceItemID = 37 };
context.ServiceItems.Attach(serviceItem2);
service.ServiceItems.Add(serviceItem1);
service.ServiceItems.Add(serviceItem2);
context.SaveChanges();
}
For completeness: How to remove relationships between existing entities?
using (var context = new MyContext())
{
var service = context.Services
.Include(s => s.ServiceItems) // load the existing Items
.Single(s => s.ServiceID == 15);
var serviceItem1 = service.ServiceItems
.Single(s => s.ServiceItemID == 23); // query in memory, no DB query
var serviceItem2 = service.ServiceItems
.Single(s => s.ServiceItemID == 37); // query in memory, no DB query
service.ServiceItems.Remove(serviceItem1);
service.ServiceItems.Remove(serviceItem2);
context.SaveChanges();
}
The two relationship rows in the join table that link service 15 with serviceItem 23 and 37 will be deleted.
Alternativly instead of calling Attach you can load the existing entities from the database. It will work as well:
var service = context.Services.Single(s => s.ServiceID == 15);
And the same for existing ServiceItems.

Insertion order of multiple records in Entity Framework

I'm having trouble with EF reordering my inserts when I try and add an entity with multiple children all at once. I've got a 3 level structure with one-to-many relationships between each (Outer 1--* Item 1--* SubItem). If I try and insert a new Outer with Items and Subitems, the Items which contain SubItems end up being inserted first.
Sample Code (.NET 4.5, EF 5.0.0-rc):
public class Outer
{
public int OuterId { get; set; }
public virtual IList<Item> Items { get; set; }
}
public class Item
{
public int OuterId { get; set; }
[ForeignKey("OuterId")]
public virtual Outer Outer { get; set; }
public int ItemId { get; set; }
public int Number { get; set; }
public virtual IList<SubItem> SubItems { get; set; }
}
public class SubItem
{
public int SubItemId { get; set; }
[ForeignKey("ItemId")]
public virtual Item Item { get; set; }
public int ItemId { get; set; }
}
public class MyContext : DbContext
{
public DbSet<Outer> Outers { get; set; }
public DbSet<Item> Items { get; set; }
public DbSet<SubItem> SubItems { get; set; }
}
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new DropCreateDatabaseAlways<MyContext>());
MyContext context = new MyContext();
// Add an Outer object, with 3 Items, the middle one having a subitem
Outer outer1 = new Outer { Items = new List<Item>() };
context.Outers.Add(outer1);
outer1.Items.Add(new Item { Number = 1, SubItems = new List<SubItem>() });
outer1.Items.Add(new Item { Number = 2, SubItems = new List<SubItem>(new SubItem[] { new SubItem() }) });
outer1.Items.Add(new Item { Number = 3, SubItems = new List<SubItem>() });
context.SaveChanges();
// Print the order these have ended up in
foreach (Item item in context.Items)
{
Console.WriteLine("{0}\t{1}", item.ItemId, item.Number);
}
// Produces output:
// 1 2
// 2 1
// 3 3
}
}
I'm aware of this answer by Alex James which states that inserts may need to be reordered in order to satisfy relational constraints, but that is not the issue here. His answer also mentions that they can't track the order of items in order-preserving structures such as Lists.
What I'd like to know is how I can get these inserts to be ordered. While I can rely on sorting my inserted items by a field other than the PK, it's a lot more efficient if I can rely on the PK order. I don't really want to have to use multiple SaveChanges calls to accomplish this.
I'm using EF5 RC, but judging by the other unanswered questions around, this has been around for some time!
What I'd like to know is how I can get these inserts to be ordered.
You cannot. Order of database commands is EF's internal behavior. If you want to control the order of commands don't use tools which abstract you from low level database interactions - use SQL directly.
Edit based on comment:
Yes it is low level interaction because you are putting expectations on the order of SQL commands when working with abstraction you don't have under your control. At high level you are getting something different because you are using expectations which don't work with that abstraction. If you want to have control over order of SQL commands you must either force EF by saving items one by one (=> multiple SaveChanges and TransactionScope) or write SQL yourselves. Otherwise use separate column for ordering.
Btw. EF doesn't save the entity as you see it. It has its own change tracker holding references to all your attached instances. References are held in multiple Dictionary instances and dictionary doesn't preserve insertion order. If these collections are used for generating SQL commands (and I guess they are) no order can be guaranteed.
Tables in the database are sets. That means that the order is not guaranteed. I assume in your example that you want the results ordered by "Number". If that is what you want, what are you going to do if that number changes and it doesn't reflect the order in the database anymore?
If you really want to have the rows inserted in a specific order, multiple SaveChanges are your best bet.
The reason nobody wants to call SaveChanges multiple times is because this feels exactly how it is: a dirty hack.
Since a primary key is a technical concept, it shouldn't make any functional sense to order your results on this key anyway. You can order the results by a specific field and use a database index for this. You probably won't see the difference in speed.
Making the ordering explicit has other benefits as well:
it is easier to understand for people who have to maintain it. Otherwise that person has to know that ordering on primary key is important and gives the correct results, because in an other (completely) unrelated section of your application, it accidentally is the same order as the number field.
Another way of doing this, without database round trip after each entry added (heavily dependent on the application logic though) is via combination of entity state changes.
In my case - hierarchy of nodes - I had to persist root nodes first, then rest of the hierarchy in order to automatic path calculations to work.
So I had a root nodes, without parent ID provided and child nodes with parent ID provided.
EF Core randomly (or through complex and intelligent logic - as you prefer :) randomly scheduled nodes for insertion, breaking path calculation procedure.
So I went with overriding SaveChanges method of the context and inspecting entities from the set for which I need to maintain certain order of inserts - detaching any child nodes first, then saving changes, and attaching child nodes and saving changes again.
// select child nodes first - these entites should be added last
List<EntityEntry<NodePathEntity>> addedNonRoots = this.ChangeTracker.Entries<NodePathEntity>().Where(e => e.State == EntityState.Added && e.Entity.NodeParentId.HasValue == true).ToList();
// select root nodes second - these entities should be added first
List<EntityEntry<NodePathEntity>> addedRoots = this.ChangeTracker.Entries<NodePathEntity>().Where(e => e.State == EntityState.Added && e.Entity.NodeParentId.HasValue == false).ToList();
if (!Xkc.Utilities.IsCollectionEmptyOrNull(addedRoots))
{
if (!Xkc.Utilities.IsCollectionEmptyOrNull(addedNonRoots))
{
// detach child nodes, so they will be ignored on SaveChanges call
// no database inserts will be generated for them
addedNonRoots.ForEach(e => e.State = EntityState.Detached);
// run SaveChanges - since root nodes are still there,
// in ADDED state, inserts will be executed for these entities
int detachedRowCount = base.SaveChanges();
// re-attach child nodes to the context
addedNonRoots.ForEach(e => e.State = EntityState.Added);
// run SaveChanges second time, child nodes are saved
return base.SaveChanges() + detachedRowCount;
}
}
This approach does not let you preserve order of individual entities, but if you can categorize entities in those that must be inserted first, and those than can be inserted later - this hack may help.
I've found a way to do it. It just thought I'd let you know:
using (var dbContextTransaction = dbContext.Database.BeginTransaction())
{
dbContext.SomeTables1.Add(object1);
dbContext.SaveChanges();
dbContext.SomeTables1.Add(object2);
dbContext.SaveChanges();
dbContextTransaction.Commit();
}
multiple Add() before a save or AddRange() before a save does not preserve order. Also when you are reading the collection it is not guaranteed to return the results in the same order they were originally added. You need to add some property to your entities and when you query use OrderBy() to ensure they come back in the order you want.
It's not professional. But, I solved my problem with this method.
PublicMenu.cs file:
public class PublicMenu
{
public int Id { get; set; }
public int? Order { get; set; }
public int? ParentMenuId { get; set; }
[ForeignKey("ParentMenuId")]
public virtual PublicMenu Parent { get; set; }
public virtual ICollection<PublicMenu> Children { get; set; }
public string Controller { get; set; }
public string Action { get; set; }
public string PictureUrl { get; set; }
public bool Enabled { get; set; }
}
PublicMenus.json file
[
{
"Order": 1,
"Controller": "Home",
"Action": "Index",
"PictureUrl": "",
"Enabled": true
},
{
"Order": 2,
"Controller": "Home",
"Action": "Services",
"PictureUrl": "",
"Enabled": true
},
{
"Order": 3,
"Controller": "Home",
"Action": "Portfolio",
"PictureUrl": "",
"Enabled": true
},
{
"Order": 4,
"Controller": "Home",
"Action": "About",
"PictureUrl": "",
"Enabled": true
},
{
"Order": 5,
"Controller": "Home",
"Action": "Contact",
"PictureUrl": "",
"Enabled": true
}
]
DataContextSeed file
public class DataContextSeed
{
public static async Task SeedAsync(ICMSContext context, ILoggerFactory loggerFactory)
{
try
{
if (!context.PublicMenus.Any())
{
var publicMenusData = File.ReadAllText("../Infrastructure/Data/SeedData/PublicMenus.json");
var publicMenus = JsonSerializer.Deserialize<List<PublicMenu>>(publicMenusData);
foreach (var item in publicMenus)
{
context.PublicMenus.Add(item);
await context.SaveChangesAsync();
}
}
}
catch (Exception ex)
{
var logger = loggerFactory.CreateLogger<ICMSContextSeed>();
logger.LogError(ex.Message);
}
}
}
Crap!
The question is asked 9 years ago, and Microsoft still not doing anything with it.
In EF6, I found if I use
for(int i=0; i<somearray.Length; i++)
db.Table.Add(new Item {Name = somearray[i]});
It did in order, but if I use
foreach(var item in somearry)
db.Table.Add(new Item {Name = item})
then not in order.
When I used EF core 3.1, it is always not in order of the above two scenario.

RIA Services: Inserting multiple presentation-model objects

I'm sharing data via RIA services using a presentation model on top of LINQ to SQL classes. On the Silverlight client, I created a couple of new entities (album and artist), associated them with each other (by either adding the album to the artist's album collection, or setting the Artist property on the album - either one works), added them to the context, and submitted changes.
On the server, I get two separate Insert calls - one for the album and one for the artist. These entitites are new so their ID values are both set to the default int value (0 - keep in mind that depending on my DB, this could be a valid ID in the DB) because as far as I know you don't set IDs for new entities on the client. This all would work fine if I was transferring the LINQ to SQL classes via my RIA services, because even though the Album insert includes the Artist and the Artist insert includes the Album, both are Entities and the L2S context recognizes them. However, with my custom presentation model objects, I need to convert them back to the LINQ to SQL classes maintaining the associations in the process so they can be added to the L2S context.
Put simply, as far as I can tell, this is impossible. Each entity gets its own Insert call, but there's no way you can just insert the one entity because without IDs the associations are lost. If the database used GUID identifiers it would be a different story because I could set those on the client.
Is this possible, or should I be pursuing another design?
If you create the correct parent-child associations, you'll just need to track the inserted presentation model(PM)-entity relationships:
PM's:
public class Parent
{
[Key]
public int? ParentID { get; set; }
[Include]
[Composition]
[Association("Parent_1-*_Child", "ParentID", "ParentID", IsForeignKey = false)]
public IEnumerable<Child> Children { get; set; }
}
public class Child
{
[Key]
public int? ChildID { get; set; }
[Include]
[Association("Parent_1-*_Child", "ParentID", "ParentID", IsForeignKey = true)]
public Parent Parent { get; set; }
}
Be sure to use [Composition] to force WCF RIA to call the InsertChild method on the DomainService.
Silverlight:
...
public Child NewChild(Parent parent)
{
return new Child
{
ParentID = parent.ParentID,
Parent = parent,
};
}
...
public void SubmitChanges()
{
DomainContext.SubmitChanges(SaveComplete, null);
}
...
If the Parent is not new, it will have a ParentID. If it is new, the Parent ID will be null. By setting the Child.Parent to the reference of the new Parent, RIA understands what you are trying to do preserves the reference after it has been sent to the server.
DomainService on the server:
[EnableClientAccess]
public class FamilyDomainService : DomainService
{
private readonly IDictionary<object, EntityObject> _insertedObjectMap;
public void InsertParent(Parent parent)
{
ParentEntity parentEntity = new ParentEntity();
ObjectContext.AddToParents(parentEntity);
_insertedObjectMap[parent] = parentEntity;
ChangeSet.Associate(parent, parentEntity, (p, e) => p.ParentID = e.ParentID;
}
public void InsertChild(Child child)
{
var childEntity = new ChildEntity();
if (child.ParentID.HasValue) // Used when the Parent already exists, but the Child is new
{
childEntity.ParentID = child.ParentID.GetValueOrDefault();
ObjectContext.AddToChildren(childEntity);
}
else // Used when the Parent and Child are inserted on the same request
{
ParentEntity parentEntity;
if (child.Parent != null && _insertedObjectMap.TryGetValue(child.Parent, out parentEntity))
{
parentEntity.Children.Add(childEntity);
ChangeSet.Associate(child, childEntity, (c, e) => c.ParentID = e.Parent.ParentID);
}
else
{
throw new Exception("Unable to insert Child: ParentID is null and the parent Parent cannot be found");
}
}
_insertedObjectMap[child] = childEntity;
ChangeSet.Associate(child, childEntity, (c, e) => c.ChildID = e.ChildID );
}
protected override bool PersistChangeSet()
{
ObjectContext.SaveChanges();
_insertedObjectMap.Clear();
return true;
}
}
The two important pieces here. First, the '_insertedObjectMap' stores the relationship between newly inserted entities that do not have the ID set. Since you are doing this in a transaction and single call to the DB, the ID will only be set after all entities have been inserted. By storing the relationship, the Child PM can find the entity version of the Parent PM using the database. The Child entity is added to the Children collection on the Parent entity and LINQToSQL or LINQToEnityFramework should handle the foreign key for you.
The second piece is associating the changes after the transaction is committed. In the scenario where the Parent and Child are both submitted, you must remember to set the ParentID foreign key on the Child.
My info from the ChangeSet.Associate() came from: http://blogs.msdn.com/deepm/archive/2009/11/20/wcf-ria-services-presentation-model-explained.aspx