Entity Framework & RIA Services - CRUD on Link Table

143 views Asked by At

After many hours, countless failures, I decided to change my Entity Model to include a link table in the model for each many-to-many relationship. This worked for me because RIA Services doesn't support many-to-many relationships.

Regardless, I'm able to build, but do not have any idea how to manage these relationships within the application itself. Should I create methods on the Domain Service, that are hidden from the client and used to perform CRUD operations on the link table objects?

An example would be greatly appreciated, thanks in advance.

2

There are 2 answers

0
mCasamento On BEST ANSWER

I guess you already know http://m2m4ria.codeplex.com/ that adds many to many support to wcf ria services, however if you want to manage it by yourself, you better send it to the client and treat them like any other entities.
You will not have Entity A with a collection of B entities and entities B with a collection of A entitities but rather:

public class A
{
    int Id {get; set;}
    ICollection<A_To_B> B_Entities {get; private set;}
}
public class A_To_B
{
    int Id {get; set;}
    A EntityA {get; set;}
    int id_A {get; set;}
    B EntityB {get; set;}
    int id_B {get; set;}
}
public class B
{
    int Id {get; set;}
    ICollection<A_To_B> A_Entities {get; private set;}
}

in your domain service add methods to correctly expose all of these entities and don't forget to properly decorate them (relationship is straight 1:m)

0
John On

This is indeed a nuisance.

I've not tried m2m4ria and do it manually on the client, ie. I expose the bridge table in the domain service. Sometimes it turns out to be a good idea anyway if the bridge table is later elevated to carry more data.

To ease the pain of managing the bridge table on the client I've written some helper you might want to consider yourself.

    public interface ILinkEntity
        where LinkEntity : Entity, ILinkEntity
        where SourceEntity : Entity, ILinkedSourceEntity
        where TargetEntity : Entity
    {
        SourceEntity Source { get; set; }
        TargetEntity Target { get; set; }
    }

    public interface ILinkedSourceEntity
        where SourceEntity : Entity, ILinkedSourceEntity
        where LinkEntity : Entity, ILinkEntity
        where TargetEntity : Entity
    {
        EntityCollection Links { get; }

        ObservableCollection Targets { get; set; }
    }

    public static class ManyToManyHelper
    {
        public static void UpdateLinks(this ILinkedSourceEntity source, EntitySet set)
            where SourceEntity : Entity, ILinkedSourceEntity
            where LinkEntity : Entity, ILinkEntity, new()
            where TargetEntity : Entity
        {
            if (!(source is SourceEntity)) throw new Exception("Expected source to be a SourceEntity.");

            var toAdd = (
                from target in source.Targets
                where source.Links.FirstOrDefault(le => le.Target.Equals(target)) == null
                select target
                ).ToArray();

            foreach (var target in toAdd) source.Links.Add(new LinkEntity() { Source = source as SourceEntity, Target = target });

            var toRemove = (
                from link in source.Links
                where source.Targets.FirstOrDefault(te => te.Equals(link.Target)) == null
                select link
                ).ToArray();

            foreach (var link in toRemove)
            {
                source.Links.Remove(link);
                // This can happen when the entities had not yet been added to the context.
                set.Remove(link);
            }
        }

        public static void UpdateTargets(this ILinkedSourceEntity source)
            where SourceEntity : Entity, ILinkedSourceEntity
            where LinkEntity : Entity, ILinkEntity, new()
            where TargetEntity : Entity
        {
            if (source.Targets == null)
            {
                source.Targets = new ObservableCollection();
            }
            else
            {
                source.Targets.Clear();
            }

            foreach (var link in source.Links) source.Targets.Add(link.Target);
        }
    }

I have this in a file called ManyToManyUtils and it should live somewhere where your domain entities can reference them (so typically in the domain client project).

I then augment the respective auto-generated domain entities to support those interfaces, eg. like this:

    public partial class Question : ILinkedSourceEntity
    {
        EntityCollection ILinkedSourceEntity.Links
        {
            get { return QuestionCategories; }
        }

        public ObservableCollection Categories { get; set; }

        ObservableCollection ILinkedSourceEntity.Targets
        {
            get { return Categories; }
            set { Categories = value; }
        }
    }

    public partial class QuestionCategory : ILinkEntity
    {
        Question ILinkEntity.Source { get { return Question; } set { Question = value; } }

        Category ILinkEntity.Target { get { return Category; } set { Category = value; } }
    }

    public partial class Category
    {
    }

So in this example each Question can be in many categories. Category as a domain entity needs not to be modified.

I usually augment domain entity classes with properties frequently anyway, so I often already have those partial classes.

Now I can bind views against those new collection properties. However, I still need to call the helper update methods to sync the bridge table with those helper collection properties.

So after each load or refresh from the domain services you have to call:

    myQuestion.UpdateTargets();

And after each edit by the user (eg from a SelectionChanged handler in the view, or - if you are happy with the consequences - just before you call SaveChanges), call:

    myQuestion.UpdateLinks(myContext.QuestionCategories);

That way, the nastiness is factored out as much as possible.