Silverlight RIA Services - Extending an EF4 Database Model with 'POCO' entities

Introduction

Recently when working with a Silverlight 4 RIA services client application, I stumbled upon a need to augment the RIA services with some objects that weren't derived directly from database tables or views, and I had no desire to write a SQL stored procedure to perform the complicated calculations necessary to build the return values.  Of course, RIA services doesn't particularly like serving up these types of objects, or more accurately, the tooling doesn't support such behavior out of the box.  I also didn't like the idea of throwing up a separate WCF service to serve the non-model derived objects either, as it presented two separate interfaces to the client code, rather than the single unified application service interface that RIA services provides.

I had a few requirements for the returned objects:

  1. The class should provide a navigation property to a parent object which exists in the model [client and service side]
  2. The objects would not be saved/persisted to the database
  3. The client-side representation of the parent class should have a navigation property to the child object to assist in binding the UI
  4. The class required an enumeration property

 

Experimentation

So I began walking down the path of adding some simple objects to the EF4 model which were not mapped to any underlying data store.  It turns out that, although it should be possible and can actually be compiled without errors [but with warnings], the EF4 infrastructure does not allow you to have a 'partially database derived' model.  As soon as you do so, runtime errors are thrown, even in situations where the non-database objects are not referenced.  So that pretty much landed me in a dead end.  Strike one...

I had already ruled out the use of two separate models, since I wasn't certain how RIA services would handle that, and I didn't think that navigation properties between the models would be possible.  At some point in the future, I may walk down that particular path, but likely in another form [and another post]. Strike two...

The third attempt was to have the RIA service return an IEnumerable<> of the appropriate POCO objects.  Unfortunately, there really isn't any way to generate the code around this, since the code generation tooling expects an underlying model to exist to generate the code from.  What resulted is a little ugly to look at, but it's dead simple to USE on the client-side and doesn't fall outside the normal usage of the RIA entity patterns.  I've outlined the solution below in the hopes that it can lead to an even better solution long term.

 

The Solution

Throughout the rest of the article I’ll be utilizing the ‘Chinook’ database available on CodePlex here.

The Model

We'll start with the easy part, establishing the base class which can be used for deriving all our specific POCO objects.  In this example, we'll call it PocoDataBase.  The purpose of the ‘Data’ classes will be to return the total number of milliseconds and bytes for a given artist, album, genre, etc as a summary rollup [so we’ll end up with AlbumData, ArtistData, etc].  To facilitate the retrieval of the summary data, we’ll add two views, TrackDataView and PlaylistDataView [the view creation script is here].  We’ll be doing the actual summarization in code, not in the database, to demonstrate the concept, not because it’s a good idea to do in this case.

I won’t walk you through the steps in creating a model from the Chinook data base, Chris Woodruff [@cwoodruff] made a great presentation on just this topic and the slide deck/code can be found here.  I will assume at this point that you have a baseline, RIA Services enabled project, but that you’ve only gone so far as to create the model from the Chinook database [including the two views mentioned above] and pick up from there.

First, we’ll add two PocoDataBase files, a shared one which will flow down to the Silverlight client, and another which will define the ‘base’ properties for all ‘Data’ objects.  The relevant code is here:

Models\PocoDataBase.cs

    [Serializable()]
    [DataContractAttribute(IsReference=true)]
    public abstract partial class PocoDataBase : EntityObject
    {
        /// 
        /// No Metadata Documentation available.
        /// 
        [DataMemberAttribute()]
        [KeyAttribute()]
        public global::System.Guid DataId
        {
            get
            {
                return _DataId;
            }
            set
            {
                OnDataIdChanging(value);
                ReportPropertyChanging("DataId");
                _DataId = StructuralObject.SetValidValue(value);
                ReportPropertyChanged("DataId");
                OnDataIdChanged();
            }
        }
        private global::System.Guid _DataId;
        partial void OnDataIdChanging(global::System.Guid value);
        partial void OnDataIdChanged();

        /// 
        /// No Metadata Documentation available.
        /// 
        [DataMemberAttribute()]
        public global::System.Int32 Milliseconds
        {
            get
            {
                return _Milliseconds;
            }
            set
            {
                OnMillisecondsChanging(value);
                ReportPropertyChanging("Milliseconds");
                _Milliseconds = StructuralObject.SetValidValue(value);
                ReportPropertyChanged("Milliseconds");
                OnMillisecondsChanged();
            }
        }
        private global::System.Int32 _Milliseconds;
        partial void OnMillisecondsChanging(global::System.Int32 value);
        partial void OnMillisecondsChanged();

        /// 
        /// No Metadata Documentation available.
        /// 
        [DataMemberAttribute()]
        public Nullable<global::system.int32> Bytes
        {
            get
            {
                return _Bytes;
            }
            set
            {
                OnBytesChanging(value);
                ReportPropertyChanging("Bytes");
                _Bytes = StructuralObject.SetValidValue(value);
                ReportPropertyChanged("Bytes");
                OnBytesChanged();
            }
        }
        private Nullable<global::system.int32> _Bytes;
        partial void OnBytesChanging(Nullable<global::system.int32> value);
        partial void OnBytesChanged();

        [DataMember]
        public int LengthValue
        {
            get { return _lengthValue; }
            set { _lengthValue = value; }
        }
        private int _lengthValue;

        [DataMember]
        public int SizeValue
        {
            get { return _sizeValue; }
            set { _sizeValue = value; }
        }
        private int _sizeValue;
    }

 

Models\PocoDataBase.shared.cs

    public enum Length
    {
        Unknown = -1,
        Blip = 0, // 10 seconds
        Short = 120000, // 2 minutes
        Normal = 210000, // 3.5 minutes
        Long = 300000, // 5 minutes
        Obscene = 480000 // 8 minutes
    }

    public enum Size
    {
        Unknown = -1,
        Tiny = 0,
        Small = 102400, // 100K
        Medium = 1048576, // 1MB
        Large = 5242880, // 5 MB
        Immense = 10485760 // 10MB
    }

    public partial class PocoDataBase
    {
        public Length Length
        {
            get { return (Length)LengthValue; }
            set { LengthValue = (int)value; }
        }

        public Size Size
        {
            get { return (Size)SizeValue; }
            set { SizeValue = (int)value; }
        }
    }

Then, we'll add a single AlbumData.cs class to represent our first concrete data class. This class will contain the AlbumId, Title and a reference to the server-side Album class that represents that Album.

Models\AlbumData.cs

    [Serializable()]
    [DataContractAttribute(IsReference = true)]
    public partial class AlbumData : PocoDataBase
    {
        /// 
        /// No Metadata Documentation available.
        /// 
        [DataMemberAttribute()]
        public global::System.Int32 AlbumId
        {
            get
            {
                return _AlbumId;
            }
            set
            {
                if (_AlbumId != value)
                {
                    OnAlbumIdChanging(value);
                    ReportPropertyChanging("AlbumId");
                    _AlbumId = StructuralObject.SetValidValue(value);
                    ReportPropertyChanged("AlbumId");
                    OnAlbumIdChanged();
                }
            }
        }
        private global::System.Int32 _AlbumId;
        partial void OnAlbumIdChanging(global::System.Int32 value);
        partial void OnAlbumIdChanged();

        [XmlIgnore()]
        [SoapIgnore()]
        [DataMember()]
        [Association("AlbumDataAlbum", "AlbumId", "AlbumId", IsForeignKey = true)]
        public Album Album { get; set; }

        [Browsable(false)]
        [DataMember()]
        public EntityReference<Album> AlbumReference { get; set; }

        /// 
        /// No Metadata Documentation available.
        /// 
        [DataMemberAttribute()]
        public global::System.String Title
        {
            get
            {
                return _Title;
            }
            set
            {
                OnTitleChanging(value);
                ReportPropertyChanging("Title");
                _Title = StructuralObject.SetValidValue(value, false);
                ReportPropertyChanged("Title");
                OnTitleChanged();
            }
        }
        private global::System.String _Title;
        partial void OnTitleChanging(global::System.String value);
        partial void OnTitleChanged();

    }

 

The Service

Now, we can add the actual RIA Services instance [or if you've added it already, simply delete the current one and add a new one. Remember to recompile your project before adding the service, or you won't see any changes you've made. During generation of the RIA service [Domain Service], you won't see the PocoDataBase and AlbumData entities, since they don't exist in your model proper. Generate the service [I called mine ChinookDomainService], and then re-build the project.

SIDEBAR: At this point, it's probably worth mentioning my previous article on RIA Services code [re]generation. The article helps to explain some of the organization concepts that you'll see in the following explanation.

One thing you'll notice if you immediately compile the application is that it doesn't compile. That's because we've told the RIA services infrastructure to copy our PocoDataBase.shared.cs file down the client, but then we never told it to expose the PocoDataBase class in the first place, so there is nothing for the .shared.cs class to extend. This is solved in at least two ways, the first is to create the 'PocoDataBase' entity in your model and do the song-and-dance that's required to actually make the model compile successfully [this will have the side effect of having the PocoDataBase and AlbumData classes appear in the RIA Services generation wizard]. This is the approach I took in my earlier project(s), but as mentioned in the investigation portion above, that gets too complicated [and it doesn't really express the intent that the class doesn't really exist in the database model], so I've opted to take a second approach. The second approach is to simply add a dummy method to the ChinookDomainService.extensions.cs file to expose an IQueryable returning method. This will be sufficient to get everything to compile for now.

The next step is to expose our AlbumData class to the Silverlight client so we can retrieve the rolled up summary information for display on our simple UI. To do this, we need to expose a method to retrieve the summary information, for this example, I'll provide a simple method that returns all summary information for all albums. The method looks something like this:

Services\ChinookDomainService.extensions.cs

    public IEnumerable<AlbumData> GetAlbumData()
    {
        var trackData = ObjectContext.TrackDataViews;

        List<AlbumData> retVal = new List<AlbumData>();
        foreach (int albumId in trackData.Select(i => i.AlbumId).Distinct())
        {
            retVal.Add(GetAlbumData(albumId, trackData.Where(i => i.AlbumId == albumId)));
        }

        return retVal;
    }

    private AlbumData GetAlbumData(int albumId, IEnumerable<TrackDataView> tracks)
    {
        const int AVERAGE_TRACKS_PER_ALBUM = 9;

        AlbumData retVal = new AlbumData() { DataId = Guid.NewGuid(), AlbumId = albumId, Bytes = 0, Milliseconds = 0 };

        foreach (TrackDataView track in tracks)
        {
            retVal.Title = track.AlbumTitle;
            retVal.Bytes += track.Bytes;
            retVal.Milliseconds += track.Milliseconds;
        }

        // Assign Length Enumeration
        if (retVal.Milliseconds < (int)Length.Short * AVERAGE_TRACKS_PER_ALBUM) retVal.Length = Length.Blip;
        else if (retVal.Milliseconds < (int)Length.Normal * AVERAGE_TRACKS_PER_ALBUM) retVal.Length = Length.Short;
        else if (retVal.Milliseconds < (int)Length.Long * AVERAGE_TRACKS_PER_ALBUM) retVal.Length = Length.Normal;
        else if (retVal.Milliseconds < (int)Length.Obscene * AVERAGE_TRACKS_PER_ALBUM) retVal.Length = Length.Long;
        else retVal.Length = Length.Obscene;

        // Assign Size Enumeration
        if (retVal.Bytes < (int)Size.Small * AVERAGE_TRACKS_PER_ALBUM) retVal.Size = Size.Tiny;
        else if (retVal.Bytes < (int)Size.Medium * AVERAGE_TRACKS_PER_ALBUM) retVal.Size = Size.Small;
        else if (retVal.Bytes < (int)Size.Large * AVERAGE_TRACKS_PER_ALBUM) retVal.Size = Size.Medium;
        else if (retVal.Bytes < (int)Size.Immense * AVERAGE_TRACKS_PER_ALBUM) retVal.Size = Size.Large;
        else retVal.Size = Size.Immense;

        return retVal;
    }

Because the method exposes an IEnumerable it automatically gets considered a 'Query' type method. So accessing it from the client is as simple as calling 'DomainContext.Load(DomainContext.GetAlbumDataQuery(), ...)'. However, right away you'll encounter a couple compilation errors when you try to build the solution. One relates to the underlying WCF technology used with RIA Services, we need to tell WCF that we can send/receive PocoDataBase instances which are actually AlbumData instances. We do this through the 'KnownType' attribute on the server-side PocoDataBase class. The updated class declaration looks like this:

    [Serializable()]
    [DataContractAttribute(IsReference=true)]
    [KnownType(typeof(AlbumData))]
    public partial class PocoDataBase : EntityObject
    {

The second is a new error that I hadn't encountered on previous projects, and may relate to the fact that my PocoDataBase class does not actually exist on the EF4 model. This error relates to the fact that the 'AlbumData.AlbumReference' property on the service side is not a supported type for client-side code generation. This can be solved easily enough by introducing some RIA services metadata for the AlbumData class. I'm not going into detail about the metadata infrastructure in this post, so I'll just post the two metadata classes that are needed here. IMPORTANT: These classes should be in the 'Models' namespace to work properly.

Services\Metadata\PocoDataBase.metadata.cs

    [MetadataType(typeof(PocoDataBase.PocoDataBaseMetadata))]
    public partial class PocoDataBase
    {
        internal class PocoDataBaseMetadata
        {
            protected PocoDataBaseMetadata()
            {

            }

            public long Bytes { get; set; }

            public Guid DataId { get; set; }

            public int LengthValue { get; set; }

            public long Milliseconds { get; set; }

            public int SizeValue { get; set; }
        }
    }

 

Services\Metadata\AlbumData.metadata.cs

    [MetadataType(typeof(AlbumData.AlbumDataMetadata))]
    public partial class AlbumData : PocoDataBase
    {
        internal sealed class AlbumDataMetadata
        {
            private AlbumDataMetadata()
            {

            }

            public Album Album { get; set; }

            [Exclude]
            public EntityReference<Album> AlbumReference { get; set; }

            public int AlbumId { get; set; }

            public string Title { get; set; }
        }
    }

The key here is the 'Exclude' attribute, which tells RIA services to avoid generating code for the service-side property on the client. We don't need it here [and don't really need it on the server in this example, but some times it is necessary, so I'll leave it in]. Once we've made these two changes, everything should compile properly. Of course, now we need some way of accessing that 'AlbumData' type from an individual Album class, and to do that, we'll introduce some client-side extensions to the Album class [and the AlbumData class].

 

The Client

On the client-side, we'd like to have a property that can navigate from the Album class to the associated AlbumData class [if loaded] and which gets notified when the data is loaded, since we'll be loading the Album instances and the AlbumData instances from two different service calls. To do this, we'll introduce some client-side extensions to the Silverlight project. The first is the simple one, we want to have a concept of an 'Unknown' AlbumData instance which can be returned by the Album instance before any AlbumData has been loaded. To accomodate this, we'll add a very simple extension to the client:

Models\AlbumData.extensions.cs

    public partial class AlbumData
    {
        public static AlbumData Empty = new AlbumData() { Length = Length.Unknown, Size = Size.Unknown };
    }

This and all other extension classes should reside in the '[Project].Web.Models' namespace, so that the partials match up with the RIA Service generated code. The second extension is a little more complicated, as it sets up the navigation property between the Album and the AlbumData instances. The code looks like this:

Models\Album.extensions.cs

    public partial class Album
    {
        private EntityRef<AlbumData> _data;

        [Association("AlbumAlbumData", "AlbumId", "AlbumId", IsForeignKey = false)]
        [XmlIgnore()]
        public AlbumData Data
        {
            get
            {
                if ((this._data == null))
                {
                    this._data = new EntityRef<AlbumData>(this, "Data", this.FilterAlbumData);
                }

                if (this._data.Entity == null)
                {
                    return AlbumData.Empty;
                }
                else
                {
                    return this._data.Entity;
                }

            }
            set
            {
                AlbumData previous = this.Data;
                if (value != previous)
                {
                    _data.Entity = value;
                    RaisePropertyChanged("Data");
                }
            }
        }

        private bool FilterAlbumData(AlbumData entity)
        {
            return (entity.AlbumId == this.AlbumId);
        }

    }

This extension provides our navigation property between the Album and the AlbumData instance that represents our summary information. Now we can bind the UI to to Album.Data.Size and get proper change notifications, etc when the data is loaded asynchronously. The last step is actually hooking up the call into the service to retrieve the information. For example:

ViewModels\MainPageViewModel.cs

    public MainPageViewModel()
    {
        // ... other code

        CurrentContext.Load(
            CurrentContext.GetAlbumDataQuery(),
            LoadBehavior.MergeIntoCurrent,
            (loadOp) =>
            {
                if (!loadOp.HasError)
                {
                    foreach (AlbumData data in loadOp.Entities)
                    {
                        // only tie in the AlbumData class if the 
                        // associated Album is already in context
                        if (data.Album != null)
                        {
                            data.Album.Data = data;
                        }
                    }
                }
            },
            null);
    }

When you run the application, the albums are now loaded, and the album data instances are loaded in the background. Currently, the application loads everything at once, so it's hard to see the stuff 'flowing in' over time. I've avoided extending the application in many other ways, to avoid complicating the issue, but the concept could be extended to provide ArtistData, GenreData, etc.

Conclusion

So, how did we do in terms of the goals laid out in the beginning of the post? 

  1. The class should provide a navigation property to a parent object which exists in the model [client and service side] -- CHECK
  2. The objects would not be saved/persisted to the database -- CHECK [freebie]
  3. The client-side representation of the parent class should have a navigation property to the child object to assist in binding the UI -- CHECK
  4. The class required an enumeration property -- CHECK

To be clear, I'm not HAPPY with this particular approach, and I think, in the future, I'll likely forsake exposing database derived models through RIA services, in favor of a 'two model' approach with mapping between the Silverlight 'application service' and the underlying database service.  There will be extra code to write certainly, but it provides a nice 'cushion' to handle these types of cases.  Or maybe EF5 will let us mix POCO and database derived entities in the same model without the headaches that we see in EF4.

In the time since preparing and writing up this post, I've stumbled upon referenced data contexts, which may allow the behavior I desire without resorting to the amount of hand coding necessary here.  There isn't much that can be done about the enumerations, until they are given first-class treatment in Entity Framework, they will always need to be hacked in one fashion or another.  I continue to believe that there must be SOME way of doing what I want more simply, and I'm sure I'll find it or see it just after I post this :) [please let me know in the comments if you have better approaches that you've used successfully].

Thanks for reading! Sample code can be found here:

RIAServicesWithPoco.zip (256KB)