Entity Framework 4.1 Code First, Silverlight, and Shared Models with REST+JSON

6 Comments May 25, 2011

It's a debate that has existed for decades. The database architects insist that data is golden and the most efficient way to build software is to generate it from the database schema, while the object-oriented code junkies swear that a nice "domain model" is the way to go and that persistence is a side effect of making those objects stick around longer. Of course, if you've worked on enterprise projects long enough you'll find the answer is often somewhere in the middle: some projects benefit from a database-centric approach, others from a model-centric approach, but many require a outward in approach. Design the schemas for the database to do what it does best with relationships, design the domain model to encapsulate the data and behavior of the class, and translate one to the other (hence the ORM, or object-relational mapper).

For a very light example, consider a conference event that has speakers and sessions. Speakers may "own" several sessions they speak at, while sessions may "have" several speakers when the session is co-hosted.

The Database

In SQL this relationship would be expressed through foreign keys, often with a "link" table to facilitate the many-to-many relationship, like this:

You can easily generate this database with the following T-SQL statements:

CREATE TABLE [dbo].[tblSession] (
    [Id]          INT           IDENTITY (1, 1) NOT NULL,
    [SessionName] NVARCHAR (50) NOT NULL
);
GO
ALTER TABLE [dbo].[tblSession]
    ADD CONSTRAINT [PK_tblSession] PRIMARY KEY CLUSTERED ([Id] ASC) WITH (ALLOW_PAGE_LOCKS = ON, ALLOW_ROW_LOCKS = ON, PAD_INDEX = OFF, IGNORE_DUP_KEY = OFF, STATISTICS_NORECOMPUTE = OFF);
GO
CREATE TABLE [dbo].[tblSessionSpeaker] (
    [Id]        INT IDENTITY (1, 1) NOT NULL,
    [SessionId] INT NOT NULL,
    [SpeakerId] INT NOT NULL
);
GO
ALTER TABLE [dbo].[tblSessionSpeaker]
    ADD CONSTRAINT [PK_tblSessionSpeaker] PRIMARY KEY CLUSTERED ([Id] ASC) WITH (ALLOW_PAGE_LOCKS = ON, ALLOW_ROW_LOCKS = ON, PAD_INDEX = OFF, IGNORE_DUP_KEY = OFF, STATISTICS_NORECOMPUTE = OFF);
GO
CREATE TABLE [dbo].[tblSpeaker] (
    [Id]   INT           IDENTITY (1, 1) NOT NULL,
    [Name] NVARCHAR (50) NOT NULL
);
GO
ALTER TABLE [dbo].[tblSpeaker]
    ADD CONSTRAINT [PK_tblSpeaker] PRIMARY KEY CLUSTERED ([Id] ASC) WITH (ALLOW_PAGE_LOCKS = ON, ALLOW_ROW_LOCKS = ON, PAD_INDEX = OFF, IGNORE_DUP_KEY = OFF, STATISTICS_NORECOMPUTE = OFF);
GO
ALTER TABLE [dbo].[tblSessionSpeaker] WITH NOCHECK
    ADD CONSTRAINT [FK_tblSessionSpeaker_tblSession] FOREIGN KEY ([SessionId]) REFERENCES [dbo].[tblSession] ([Id]) ON DELETE NO ACTION ON UPDATE NO ACTION;
GO
ALTER TABLE [dbo].[tblSessionSpeaker] WITH NOCHECK
    ADD CONSTRAINT [FK_tblSessionSpeaker_tblSpeaker] FOREIGN KEY ([SpeakerId]) REFERENCES [dbo].[tblSpeaker] ([Id]) ON DELETE NO ACTION ON UPDATE NO ACTION;

The Application

However, moving over the application side, the "link table" just doesn't make sense. Relationships can be expressed as navigation properties, so that speakers contain sessions and vice versa. The model there looks more like this:

These classes are expressed using the code below. Note that the fact this example will use an object relational mapper (ORM) has already interfered with our design a bit, because for it to provide peristence ignorance the collections had to be tagged virtual:

public abstract class BaseIdentityModelCore
{
    public virtual int Id { get; set; }
}

public abstract class BaseIdentityModel<T> : BaseIdentityModelCore where T: BaseIdentityModel<T>  
{        
    public override bool Equals(object obj)
    {
        return obj is T && ((T) obj).Id.Equals(Id);
    }

    public override int GetHashCode()
    {
        return Id;
    }
}

public class Session : BaseIdentityModel<Session>
{
    public string Name { get; set; }

    public virtual List<Speaker> Speakers { get; set; }

    public void AddSpeaker(Speaker speaker)
    {
        if (Speakers == null)
        {
            Speakers = new List<Speaker>();
        }

        if (speaker.Sessions == null || !(from s in speaker.Sessions where s.Name.Equals(Name,StringComparison.InvariantCultureIgnoreCase) select 1).Any())
        {
            if (speaker.Sessions == null)
            {
                speaker.Sessions = new List<Session>();
            }
            speaker.Sessions.Add(this);
        }
            
        Speakers.Add(speaker);
    }
}

public class Speaker : BaseIdentityModel<Speaker>
{
    public string Name { get; set; }

    public virtual List<Session> Sessions { get; set; }

    public void AddSession(Session session)
    {
        if (Sessions == null)
        {
            Sessions = new List<Session>();
        }

        if (session.Speakers == null || !(from s in session.Speakers where s.Name.Equals(Name, StringComparison.InvariantCultureIgnoreCase) select 1).Any())
        {
            if (session.Speakers == null)
            {
                session.Speakers = new List<Speaker>();
            }
            session.Speakers.Add(this);
        }
            
        Sessions.Add(session);
    }
}

It's important to note that these models don't just contain data. They also contain some business logic or behavior because the methods are exposed to add children and handle the reverse navigation (i.e. if I add a speaker to a session, the session should also get added to the speaker).

The models were created in a Silverlight 3 class project. This allows the project to be shared directly in the core framework as well. Without using linked files or other exotic means, we have an assembly that is truly shared between the Silverlight client and the .NET desktop or server framework. Note also that "JounceDataGuidance" is a strong term ... this isn't meant to be total "guidance" but more an experiment and reference for one way to glue data between the client and the server.

A Business Contract

Next is a simple contract, also defined in a Silverlight 3 project so it can be shared in multiple places, to interact with the models. The methods should be self-explanatory, but note the fact that this is built with asynchronous operations in mind so it can be shared between the client and the server:

public interface ISpeakerServices
{
    void AddSpeaker(Speaker speaker, Action<int> key);
    void AddSession(Session session, Action<int> key);
    void ListSpeakers(Action<IEnumerable<Speaker>> speakers);
    void ListSessionsForSpeaker(int speakerId, Action<IEnumerable<Session>> sessions);
    void AddSessionToSpeaker(int speakerId, int sessionId);
}

Data Access

Next, we'll define a simple data access layer. This will be an abstraction to the underlying mechanism which in this case happens to be Entity Framework 4.1. Some people may argue these abstractions aren't necessary because people seldom actually switch their data provider. I disagree for two reasons. I've been on projects where switching the data provider has happened (or we've eventually had to aggregate data from multiple sources). Unit testing of a business class should never require access and setup to a database implementation. You should be able to mock the data access contract instead.

There are two contracts. The first is for a unit of work. This is a pattern that similar to the concept of a transaction. It allows you to manipulate your objects in a discrete "unit of work" and then either cancel that unit of work or commit it all at once. This prevents sending a bunch of smaller, atomic operations to the database and allows the data access layer to coordinate the changes all at once.

public interface IUnitOfWork
{
    void Commit();
    void Complete();
}

Commit will complete changes. Complete will close the connections but will not commit. It's not a "rollback" because we won't have actually interacted with the database, but it says, "complete this operation and flush any database goo without sending my changes over the wire."

The data access contract defines some common CRUD operations and assumes the models participating all use the identity structure defined in the base class:

public interface IDataAccess<T> where T: BaseIdentityModelCore
{        
    T Load(int key, bool includeChildren);
    void Save(T instance);
    void Delete(int key);
    IQueryable<T> Query(bool includeChildren);
}

Entity Framework 4.1 Code First

Now the database schema can be mapped to the domain model. The model project is referenced directly even though the data access project is on the server. While Visual Studio flags a warning due to the potential for incompatible types:



The project will work perfectly fine and compile without issue.

First, the framework is instructed how to map a speaker to the underlying database. The following configuration specifies the table and key, and also describes the many-to-many relationship between speakers and sessions with the linked table that connects the two:

public class SpeakerConfig : EntityTypeConfiguration<Speaker>
{
    public SpeakerConfig()
    {
        ToTable("tblSpeaker");
        HasKey(s => s.Id);
        Property(s => s.Id).IsRequired().HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
        HasMany(s => s.Sessions).WithMany(speaker => speaker.Speakers)
            .Map(config => config.MapLeftKey("SpeakerId").MapRightKey("SessionId").ToTable("tblSessionSpeaker"));
    }
}

If you've got a sharp eye for detail, you will have noted that the session table was defined in a "non-standard" way. The speaker simply has a "name" property, but the session uses a "sessionName" property. The following configuration handles that difference:

public class SessionConfig : EntityTypeConfiguration<Session>
{
    public SessionConfig()
    {
        ToTable("tblSession");
        HasKey(s => s.Id);
        Property(s => s.Id).IsRequired().HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
        Property(s => s.Name).HasColumnName("SessionName");
        HasMany(s => s.Speakers).WithMany(speaker => speaker.Sessions)
            .Map(config => config.MapLeftKey("SessionId").MapRightKey("SpeakerId").ToTable("tblSessionSpeaker"));
    }
}

Now that the mappings have been defined, a context for accessing the data can be created. The class defines the two collections to map as well as the configurations to map them:

public class JounceEventContext : DbContext 
{
    public DbSet<Speaker> Speakers { get; set; }
    public DbSet<Session> Sessions { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new SpeakerConfig());
        modelBuilder.Configurations.Add(new SessionConfig());
        base.OnModelCreating(modelBuilder);
    }
}

Notice all of this was done without any generated model. The next step is to implement the basic data access layer. The base class performs the common operations, and allows the derived classes to supply what the target collection is. There are two offered: a set (shallow) and a query (deep) for including children in the object graph or not.

public abstract class DataAccess<T> : IDataAccess<T> where T : BaseIdentityModelCore
{        
    protected abstract DbSet<T> GetDbSet();

    protected abstract DbQuery<T> GetDbQuery();

    public JounceEventContext Context { get; private set; }

    protected DataAccess(UnitOfWork workUnit)
    {
        Context = workUnit.Context as JounceEventContext;
    }

    public virtual IQueryable<T> Query(bool includeChildren)
    {
        return includeChildren ? GetDbQuery() : GetDbSet().AsQueryable();
    }

    private void _SetState<TBaseIdentity>(TBaseIdentity model) where TBaseIdentity : BaseIdentityModelCore
    {
        Context.Entry(model).State = model.Id > 0 ? EntityState.Modified : EntityState.Added;
    }

    public virtual T Load(int key, bool includeChildren)
    {
        return (from instance in Query(includeChildren) where instance.Id.Equals(key) select instance).FirstOrDefault();
    }

    public virtual void Save(T instance)
    {
        _SetState(instance);            
    }

    public virtual void Delete(int key)
    {
        if (GetDbSet().Where(t => t.Id.Equals(key)).Any())
        {
            return;
        }
        GetDbSet().Remove(Load(key,false));            
    }      
}

Because the objects are ignorant of persistence, we use the "set state" to communicate to the underlying context whether something needs to be inserted or updated. The fact that the navigation collections are virtual allows the Entity Framework to generate proxies and track the relationships through the link table automatically for us. You'll notice the base class requires a unit of work to be passed in, the implementation for Entity Framework looks like this:

public class UnitOfWork : IUnitOfWork 
{
    public DbContext Context { get; private set; }

    public UnitOfWork(DbContext context)
    {
        Context = context;
    }

    public void Commit()
    {
        Context.SaveChanges();
        Context.Dispose();
        Context = null;
    }

    public void Complete()
    {
        Context.Dispose();
        Context = null;
    }
}

Finally, implementing the data access is easy. If you wanted to extend the base contract with specific queries, you could, but in this case I'm simply informing the base class of the correct sets and queries to use:

public class SessionDataAccess : DataAccess<Session>
{
    public SessionDataAccess(UnitOfWork work) : base(work)
    {
            
    }

    protected override DbSet<Session> GetDbSet()
    {
        return Context.Sessions;
    }

    protected override DbQuery<Session> GetDbQuery()
    {
        return Context.Sessions.Include("Speakers");
    }
}

Speakers looks very similar so no need to post it. Notice that while this layer implements the Entity Framework and the code-first feature, the contracts for the unit of work and data access contracts are sufficiently generic enough to allow for any type of implementation. You could easily use a generate model, plug in nHibernate or use ADO and still be able to honor the contracts.

Business Layer

Now we can implement the business contract. On the server we'll reference the same contract the client will use. I'll use a simple factory to avoid having to reference the implementations directly:

public class Factory
{
    public static IUnitOfWork GetUnitOfWork()
    {
        return new UnitOfWork(new JounceEventContext());
    }

    public static IDataAccess<T> GetDataAccess<T>(IUnitOfWork work) where T: BaseIdentityModelCore
    {
        if (typeof(T).Equals(typeof(Speaker)))
        {
            return (IDataAccess<T>) new SpeakerDataAccess((UnitOfWork)work);
        }

        if (typeof(T).Equals(typeof(Session)))
        {
            return (IDataAccess<T>) new SessionDataAccess((UnitOfWork)work);
        }

        throw new TypeLoadException(string.Format("Invalid type: {0}", typeof(T).FullName));
    }
}

Then the contract can be implemented:

public class SpeakerServices : ISpeakerServices
{
    public void AddSpeaker(Speaker speaker, Action<int> key)
    {
        var unitOfWork = Factory.GetUnitOfWork();
        var dataAccess = Factory.GetDataAccess<Speaker>(unitOfWork);
        dataAccess.Save(speaker);
        unitOfWork.Commit();
        key(speaker.Id);
    }

    public void AddSession(Session session, Action<int> key)
    {
        var unitOfWork = Factory.GetUnitOfWork();
        var dataAccess = Factory.GetDataAccess<Session>(unitOfWork);
        dataAccess.Save(session);
        unitOfWork.Commit();
        key(session.Id);
    }

    public void ListSpeakers(Action<IEnumerable<Speaker>> speakers)
    {
        var unitOfWork = Factory.GetUnitOfWork();
        var dataAccess = Factory.GetDataAccess<Speaker>(unitOfWork);
        var query =
            new List<Speaker>(from speaker in dataAccess.Query(false) orderby speaker.Name select speaker);
        speakers(query);
        unitOfWork.Complete();
    }

    public void ListSessionsForSpeaker(int speakerId, Action<IEnumerable<Session>> sessions)
    {
        var unitOfWork = Factory.GetUnitOfWork();
        var dataAccess = Factory.GetDataAccess<Speaker>(unitOfWork);
        var speaker = dataAccess.Load(speakerId, true);
        var sess = speaker == null ? Enumerable.Empty<Session>() : new List<Session>(speaker.Sessions);

        var retVal = new List<Session>();
            
        // flatten for return
        foreach (var session in sess)
        {
            var sessionReturned = new Session {Id = session.Id, Name = session.Name, Speakers = new List<Speaker>()};
            foreach(var speak in session.Speakers)
            {
                sessionReturned.Speakers.Add(new Speaker {Id = speak.Id, Name = speak.Name});
            }
            retVal.Add(sessionReturned);
        }
        sessions(retVal);
        unitOfWork.Complete();
    }

    public void AddSessionToSpeaker(int speakerId, int sessionId)
    {
        var unitOfWork = Factory.GetUnitOfWork();
        var speakerAccess = Factory.GetDataAccess<Speaker>(unitOfWork);
        var sessionAccess = Factory.GetDataAccess<Session>(unitOfWork);

        var speaker = speakerAccess.Load(speakerId, true);
        var session = sessionAccess.Load(sessionId, true);

        if (session.Speakers.Contains(speaker))
        {
            unitOfWork.Complete();
            return;
        }

        session.AddSpeaker(speaker);
        sessionAccess.Save(session);

        unitOfWork.Commit();
    }
}

Notice that at this level, the objects are being interacted with directly. To add a session for a speaker, the data access layer is queried to get the latest classes, but then the behavior on the class itself is used to add a speaker to a session.

The Service Layer

Having the contract implemented is fine but we need to be able to access it from Silverlight. In this example, we'll expose the functions as REST services with JSON:

[ServiceContract(Namespace = "http://jounce.codeplex.com/services")]
[SilverlightFaultBehavior]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class SpeakerServiceEndpoint
{
    public ISpeakerServices Services = new SpeakerServices();

    [WebGet(UriTemplate = "", ResponseFormat = WebMessageFormat.Json)]
    public List<Speaker> GetSpeakers()
    {
        var speakers = new List<Speaker>();
        Services.ListSpeakers(speakers.AddRange);

        return speakers.Select(speaker => new Speaker {Id = speaker.Id, Name = speaker.Name, Sessions = new List<Session>()}).ToList();
    }

    [WebGet(UriTemplate = @"?speakerId={speakerId}", ResponseFormat = WebMessageFormat.Json)]
    public List<Session> GetSessionsForSpeaker(int speakerId)
    {
        var retVal = new List<Session>();
        Services.ListSessionsForSpeaker(speakerId, retVal.AddRange);
        return retVal;
    }

    [OperationContract]
    [WebInvoke(UriTemplate=@"AddSpeaker", BodyStyle = WebMessageBodyStyle.Bare, RequestFormat = WebMessageFormat.Json, 
        ResponseFormat = WebMessageFormat.Json)]
    public int AddSpeaker(Speaker speaker)
    {
        var retVal = 0;
        Services.AddSpeaker(speaker, key => retVal = key);
        return retVal; 
    }

    [OperationContract]
    [WebInvoke(UriTemplate = @"AddSession", BodyStyle = WebMessageBodyStyle.Bare, RequestFormat = WebMessageFormat.Json,
        ResponseFormat = WebMessageFormat.Json)]
    public int AddSession(Session session)
    {
        var retVal = 0;
        Services.AddSession(session, key => retVal = key);
        return retVal;
    }

    [OperationContract]
    [WebInvoke(UriTemplate = @"AddSessionToSpeaker?speakerId={speakerId}&sessionId={sessionId}", BodyStyle = WebMessageBodyStyle.Bare, RequestFormat = WebMessageFormat.Json,
        ResponseFormat = WebMessageFormat.Json)]
    public void AddSessionToSpeaker(int speakerId, int sessionId)
    {
        Services.AddSessionToSpeaker(speakerId, sessionId);
    }
}

Notice these services are basically just façades to the business layer. There is some definition around how the services are accessed and attributes to flag the format as JSON. The service is in a standalone class (there is not a file with the .SVC extension. The host application is MVC. To register the service we can simply add a route to the MvcApplication class. The line to add the route is:

routes.Add(new ServiceRoute("Speakers", new WebServiceHostFactory(), typeof(SpeakerServiceEndpoint)));

This establishes it with the prefix /Speakers and injects the service into the application. Now we can actually test the service end points with fiddler. A post to http://localhost/Speakers/ yields the following response (for a database that is already populated):

[{"Id":351,"Name":"Bob German","Sessions":[]},{"Id":347,"Name":"John Papa","Sessions":[]},{"Id":348,"Name":"Mike Taulty","Sessions":[]},{"Id":350,"Name":"Paul Stubbs","Sessions":[]},{"Id":349,"Name":"Ward Bell","Sessions":[]}]

Note these are actual names and sessions from the MIX 11 event. Because serialization cannot handle children that reference their parents, the sessions are passed as empty and a subsequent call grabs the sessions for a speaker: http://localhost/Speakers/?speakerId=351 (and REST purists will likely gasp and beg me NOT to use querystrings) which yields:

[{"Id":335,"Name":"Silverlight for SharePoint","Speakers":[{"Id":351,"Name":"Bob German","Sessions":null},{"Id":350,"Name":"Paul Stubbs","Sessions":null}]}]

Again, note the sessions are flattened to avoid recursion.

The Silverlight Business Layer

Silverlight can now access the exact same model project and business contract we used on the server side. There is no need to map to intermediary objects, juggle and jump through hoops. Yes, you can even share behavior without using WCF RIA!

First, we'll make a few helper classes to handle the REST calls. REST is fairly easy with Silverlight and doesn't require adding service references. Instead, we can use the WebClient.

public class WebClientHelper<T> where T : class
{
    private Action<IEnumerable<T>> _callback;
    private Action<int> _key;

    public WebClientHelper(Uri uri, Action<int> key, T instance)
    {
        _key = key;
        var wc = new WebClient();

        if (key != null)
        {
            wc.UploadStringCompleted += _WcUploadStringCompleted;
        }

        wc.Headers["Content-Type"] = "application/json";
        var serializer = new DataContractJsonSerializer(typeof (T));
        using (var memStream = new MemoryStream())
        {
            serializer.WriteObject(memStream, instance);
            memStream.Seek(0, SeekOrigin.Begin);
            using (var reader = new StreamReader(memStream))
            {
                wc.UploadStringAsync(uri, reader.ReadToEnd());
            }
        }
    }

    private void _WcUploadStringCompleted(object sender, UploadStringCompletedEventArgs e)
    {
        ((WebClient) sender).UploadStringCompleted -= _WcUploadStringCompleted;
        var serializer = new DataContractJsonSerializer(typeof (int));
        _key((int) serializer.ReadObject(new MemoryStream(Encoding.Unicode.GetBytes(e.Result))));
        _key = null;
    }

    public WebClientHelper(Uri uri, Action<IEnumerable<T>> result)
    {
        _callback = result;
        var wc = new WebClient();
        wc.DownloadStringCompleted += _WcDownloadStringCompleted;
        wc.DownloadStringAsync(uri);
    }

    private void _WcDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
    {
        ((WebClient) sender).DownloadStringCompleted -= _WcDownloadStringCompleted;
        var serializer = new DataContractJsonSerializer(typeof (List<T>));
        var results = (List<T>) serializer.ReadObject(new MemoryStream(Encoding.Unicode.GetBytes(e.Result)));
        _callback(results);
        _callback = null;
    }
}

Notice this helper handles two patterns (I know, I'm not completely separating my concerns ... but isn't the project getting big enough already?) The first is a POST where the payload is serialized in the post and a key is retrieved. The second is a GET that returns a list.

With that in place to manage our calls and turning objects to JSON and back, we can implement the same business contract we implemented on the server side:

[Export(typeof (ISpeakerServices))]
public class SpeakerServices : ISpeakerServices
{
    private const string ADD_SPEAKER_POST = "/Speakers/AddSpeaker";
    private const string ADD_SESSION_POST = "/Speakers/AddSession";
    private const string ADD_SESSION_TO_SPEAKER_POST = @"/Speakers/AddSessionToSpeaker?speakerId={0}&sessionId={1}";
    private const string LIST_SESSIONS_FOR_SPEAKER_GET = "/Speakers/?speakerId={0}";
    private const string LIST_SPEAKERS_GET = "/Speakers/";

    public void AddSpeaker(Speaker speaker, Action<int> key)
    {
        new WebClientHelper<Speaker>(new Uri(ADD_SPEAKER_POST, UriKind.Relative), key, speaker);
    }

    public void AddSession(Session session, Action<int> key)
    {
        new WebClientHelper<Session>(new Uri(ADD_SESSION_POST, UriKind.Relative), key, session);
    }

    public void AddSessionToSpeaker(int speakerId, int sessionId)
    {
        new WebClientHelper<Session>(
            new Uri(string.Format(ADD_SESSION_TO_SPEAKER_POST, speakerId, sessionId), UriKind.Relative), null,
            null);
    }

    public void ListSessionsForSpeaker(int speakerId, Action<IEnumerable<Session>> sessions)
    {
        new WebClientHelper(
            new Uri(string.Format(LIST_SESSIONS_FOR_SPEAKER_GET, speakerId), UriKind.Relative), sessions);
    }

    public void ListSpeakers(Action<IEnumerable<Speaker>> speakers)
    {
        var retVal = new List<Speaker>();
        new WebClientHelper<Speaker>(new Uri(LIST_SPEAKERS_GET, UriKind.Relative),
                                        results =>
                {
                    int[] count = {results.Count()};

                    if (count[0] == 0)
                    {
                        speakers(Enumerable.Empty<Speaker>());
                        return;
                    }

                    foreach (var speaker in results)
                    {
                        retVal.Add(speaker);
                        var speaker1 = speaker;
                        ListSessionsForSpeaker(speaker1.Id,
                                            s =>
                                                {
                                                    foreach (var session in s)
                                                    {
                                                        speaker1.AddSession(session);
                                                    }

                                                    count[0]--;
                                                    if (count[0] == 0)
                                                    {
                                                        speakers(retVal);
                                                    }
                                                });
                    }
        }
}

Notice how easy it is to just pass through to the helper for the various functions becaus they are mapped faithfully to the service layer! Now there is some hackery going on in the final list, but it is there to illustrate an important point. The speakers are returned without sessions, so an inner loop requests the sessions for each speaker. When the sessions are returned, the method on the class is used to add them so we are using the exact same behavior for the model regardless of whether we are on the server or the client - it is truly shared between both. In this case it will make sure the session is added to the speaker and then the speaker is added back to the session. A count is kept to make sure we get sessions for all speakers before the list is returned to the caller.

Now we can create a simple view model that takes advantage of the business tier and wires up the speakers and sessions:

[ExportAsViewModel(typeof(MainViewModel))]
public partial class MainViewModel : BaseViewModel, IMainViewModel
{
    [Import]
    public ISpeakerServices SpeakerServices { get; set; }

    [Import]
    public GenerateData DataGenerater { get; set; }
        
    public MainViewModel()
    {
        Speakers = new ObservableCollection<Speaker>();
        DesignerData();
    }

    public ICollection<Speaker> Speakers { get; private set; }   
             
    protected override void InitializeVm()
    {
        SpeakerServices.ListSpeakers(_GetSpeakers);
    }

    private void _GetSpeakers(IEnumerable<Speaker> speakers)
    {
        if (!speakers.Any())
        {
            WorkflowController.Begin(DataGenerater.GenerateServerDataWorkflow(SpeakerServices, 
                ()=>SpeakerServices.ListSpeakers(_GetSpeakers)));
            return;
        }
            
        foreach(var speaker in speakers)
        {
            Speakers.Add(speaker);
        }           
    }        
}

Generating Data

What about the data generation? I didn't have time to build a full CRUD example but I wanted to show inserts and updates from the client, so I decided to generate the data from the client. The first time you run the application, it will request the list of speakers and receive and empty list. It will then generate the speakers and sessions, ask the server to update them and then request the speakers list a second time. This time it will faithfully show a list of speakers and sessions (with a sub-list of speakers to show the navigation properties).

Because our data generation needs to happen sequentially, I'll take advantage of the IWorkflow interface in Jounce. I created two implementations, one to add a speaker and one to add a session. Here is what the session action looks like. It uses the business contract to add a session and then calls invoked, and the Jounce controller ensures it completes (without blocking the UI thread) before the next step is run.

public class AddSessionAction : IWorkflow
{
    private readonly ISpeakerServices _service;
    private readonly Session _session;

    public AddSessionAction(ISpeakerServices service, Session session)
    {
        _service = service;
        _session = session;
    }

    public void Invoke()
    {
        _service.AddSession(_session, key =>
        {
            _session.Id = key;
            Invoked();
        });
    }

    public Action Invoked { get; set; }
}

The workflow engine allows us to wire several sequential calls even though they are asynchronous, and essentially build the sessions and speakers we want:

public IEnumerable<IWorkflow> GenerateServerDataWorkflow(ISpeakerServices services, Action completed)
{
    var bootcamp = new Session { Name = "Silverlight Boot Camp" };
    var thumbs = new Session { Name = "All Thumbs" };
    var sharepoint = new Session { Name = "Silverlight for SharePoint" };
    var experiences = new Session { Name = "Great Experiences for SharePoint" };

    var john = new Speaker { Name = "John Papa" };
    var mike = new Speaker { Name = "Mike Taulty" };
    var ward = new Speaker { Name = "Ward Bell" };
    var paul = new Speaker { Name = "Paul Stubbs" };
    var bob = new Speaker { Name = "Bob German" };

    yield return new AddSpeakerAction(services, john);
    yield return new AddSessionAction(services, bootcamp);

    services.AddSessionToSpeaker(john.Id, bootcamp.Id);

    yield return new AddSpeakerAction(services, mike);

    services.AddSessionToSpeaker(mike.Id, bootcamp.Id);

    yield return new AddSpeakerAction(services, ward);
    yield return new AddSessionAction(services, thumbs);

    services.AddSessionToSpeaker(ward.Id, thumbs.Id);

    yield return new AddSpeakerAction(services, paul);
    yield return new AddSessionAction(services, sharepoint);
    yield return new AddSessionAction(services, experiences);

    services.AddSessionToSpeaker(paul.Id, sharepoint.Id);
    services.AddSessionToSpeaker(paul.Id, experiences.Id);

    yield return new AddSpeakerAction(services, bob);

    services.AddSessionToSpeaker(bob.Id, sharepoint.Id);

    completed();
}   

It should be very clear and natural what we are doing. Create a speaker. Create a session. Now add the session to the speaker ... and because the service layer shares the models, the behavior of the recursive links between sessions and speakers is preserved.

What does the SQL look like? We can run the profiler and see:

-- insert the speaker
exec sp_executesql N'insert [dbo].[tblSpeaker]([Name])
values (@0)
select [Id]
from [dbo].[tblSpeaker]
where @@ROWCOUNT > 0 and [Id] = scope_identity()',N'@0 nvarchar(max) ',@0=N'John Papa'

-- insert the session
exec sp_executesql N'insert [dbo].[tblSession]([SessionName])
values (@0)
select [Id]
from [dbo].[tblSession]
where @@ROWCOUNT > 0 and [Id] = scope_identity()',N'@0 nvarchar(max) ',@0=N'Silverlight Boot Camp'

-- select the session 
exec sp_executesql N'SELECT 
[Project2].[Id] AS [Id], 
[Project2].[Name] AS [Name], 
[Project2].[C1] AS [C1], 
[Project2].[Id1] AS [Id1], 
[Project2].[SessionName] AS [SessionName]
FROM ( SELECT 
 [Limit1].[Id] AS [Id], 
 [Limit1].[Name] AS [Name], 
 [Join1].[Id] AS [Id1], 
 [Join1].[SessionName] AS [SessionName], 
 CASE WHEN ([Join1].[SpeakerId] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
 FROM   (SELECT TOP (1) 
  [Extent1].[Id] AS [Id], 
  [Extent1].[Name] AS [Name]
  FROM [dbo].[tblSpeaker] AS [Extent1]
  WHERE [Extent1].[Id] = @p__linq__0 ) AS [Limit1]
 LEFT OUTER JOIN  (SELECT [Extent2].[SpeakerId] AS [SpeakerId], [Extent3].[Id] AS [Id], [Extent3].[SessionName] AS [SessionName]
  FROM  [dbo].[tblSessionSpeaker] AS [Extent2]
  INNER JOIN [dbo].[tblSession] AS [Extent3] ON [Extent3].[Id] = [Extent2].[SessionId] ) AS [Join1] ON [Limit1].[Id] = [Join1].[SpeakerId]
)  AS [Project2]
ORDER BY [Project2].[Id] ASC, [Project2].[C1] ASC',N'@p__linq__0 int',@p__linq__0=352

-- insert the session/speaker link
exec sp_executesql N'insert [dbo].[tblSessionSpeaker]([SpeakerId], [SessionId])
values (@0, @1)
',N'@0 int,@1 int',@0=352,@1=337

As you can see, very straightforward (and readable) statements, including the correct inserts for our linked table based on the mapping we declared in the configuration.

This is not much different than the process to wire the design-time data, despite the fact that the runtime process is serializing to JSON, sending over the wire using a REST service and then updating a backend database. Here is the design-time data:

public partial class MainViewModel
{
    [Conditional("DEBUG")]
    public void DesignerData()
    {
        if (!InDesigner)
        {
            return;
        }

        foreach(var speaker in _GenerateData())
        {
            Speakers.Add(speaker);
        }
    }

    private IEnumerable<Speaker> _GenerateData()
    {
        var john = new Speaker { Name = "John Papa" };
        var mike = new Speaker { Name = "Mike Taulty" };
        var ward = new Speaker { Name = "Ward Bell" };
        var paul = new Speaker { Name = "Paul Stubbs" };
        var bob = new Speaker { Name = "Bob German" };

        var bootcamp = new Session { Name = "Silverlight Boot Camp" };
        bootcamp.AddSpeaker(john);
        bootcamp.AddSpeaker(mike);
        var thumbs = new Session { Name = "All Thumbs" };
        thumbs.AddSpeaker(ward);
        var sharepoint = new Session { Name = "Silverlight for SharePoint" };
        sharepoint.AddSpeaker(paul);
        sharepoint.AddSpeaker(bob);
        var experiences = new Session { Name = "Great Experiences for SharePoint" };
        experiences.AddSpeaker(paul);
        return new[] { bob, john, mike, paul, ward };
    }

}

Now some XAML to bind it together:

<Grid x:Name="LayoutRoot" Background="White" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
        d:DataContext="{d:DesignInstance vm:MainViewModel, IsDesignTimeCreatable=True}">
    <ListBox ItemsSource="{Binding Speakers}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Grid HorizontalAlignment="Stretch">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="200"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
                    <TextBlock Text="{Binding Name}" FontSize="16" FontWeight="Bold"/>
                    <ListBox ItemsSource="{Binding Sessions}" Grid.Column="1" Margin="20 0 0 0">
                        <ListBox.ItemTemplate>
                            <DataTemplate>
                                <StackPanel HorizontalAlignment="Stretch" Orientation="Vertical">
                                    <TextBlock Text="{Binding Name}" FontWeight="Bold"/>
                                    <ItemsControl ItemsSource="{Binding Speakers}" DisplayMemberPath="Name"/>
                                </StackPanel>
                            </DataTemplate>
                        </ListBox.ItemTemplate>
                    </ListBox>
                </Grid>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

And the final result of an end-to-end project that performs CRUD operations using Entity Framework 4.1 code-first, REST services with JSON, and shares models, behavior, and contracts between the client and the server:

Grab the full project with database schema and tests here.

Jeremy Likness


6 Comments

  • Gravatar Image
    Padel June 16, 2011 4:11 AM

    Thanks for the example above. Very complex and contains many things to learn from.

  • Gravatar Image
    Dave June 24, 2011 5:37 AM

    Hey Jeremy
    Thanks for this cool blog post. I tried to follow your red line. When I try to do a webinvoke of AddPaitent (my solution) then I get an error message like follows:

    System.NotSupportedException: The URI prefix is not recognized.
    at System.Net.WebRequest.Create(Uri requestUri)
    at System.Net.WebClient.GetWebRequest(Uri address)
    at System.Net.WebClient.UploadStringAsync(Uri address, String method, String data, Object userToken)

    When I try to start your project I get the same exception on startup.
    Do I miss something?

    Thanks for anserwing.
    Kind regards.
    Dave

  • Gravatar Image
    Www July 4, 2011 10:35 AM

    Seems like a lot of code for such a simple problem.

  • Gravatar Image
    Matt Duffield August 28, 2011 9:30 PM

    Hi Jeremy,

    I like your article but I have one question. I would like to use DataAnnotations. I have ran into a problem with bringing in the Silverlight version because as soon as I do this I get a runtime exception from the Web host not being able to find the Silverlight version of System.ComponentModel. How did you overcome this? I would like to still be able to "share" my models but I need my validation as well.

    Best regards,

    Matt

  • Gravatar Image
    Tapu January 29, 2012 11:53 PM

    Plz change the color of ur site

  • Gravatar Image
    Rory February 20, 2012 3:08 AM

    Good article but shame about the use of MEF i.e. your framework
    Also, the colors of your web site, really need altering

Have a Comment?

Archives