views:

648

answers:

1

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?

+1  A: 

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

Ed Chapel
This is the answer as far as I can tell, so thank you for posting. I think I stumbled across something similar when I was working on this, but I believe that the issue was that in reality I was working with a more complicated object graph than simply a single parent/child relationship, and I don't think that this kind of solution will scale to deal with that kind of complexity.
nlawalker