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

by TheJet 13. July 2010 06:00

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)

WCF RIA Services Regeneration - Can we get a better story please?

by TheJet 29. June 2010 08:00

I've been working with the WCF RIA Services framework in Silverlight 4 now for a few months.  The theory behind it is very interesting, and I can certainly see how it gives that feeling of a true 'RIA' framework.  In general, so long as you 'stick to the path', everything just works, which is nice for once.  There certainly are problems, and I won't go into them all here.  I certainly don't have time to detail the many and varied complaints about the metadata classes that make validation work, nor do I have time to wonder why the generated domain service itself is not marked as 'partial'.  What I would like to do is add my voice to the hundreds of others who are asking, 'Why can't regeneration work?'.  Of course, just doing that is boring, so I'll make up a badly written narrative of a lonely developer working his way through his first RIA project.

DISCLAIMER:  No code monkeys were harmed in the creation of this story, the story and any project details are entirely fictional and do not represent a real world project in any way, shape or form.


Happy Times

When George first started this RIA project, he happily built his data model and added that first domain service to the project.  Everything generated beautifully, he built out the first pass at the application and all was right with the world.  George wondered about these metadata classes, but they seemed usable, if somewhat weird and constraining.  So he went about the business of adding validation rules to the metadata classes and marveled at the ease in which those error messages appeared on the UI, almost as if by magic, as he had done nothing but setup appropriate binding [remembering to include those NotifyOnValidationError and ValidatesOnExceptions attributes].

The customer was happy, George was happy, and then the first official review arrived.  Turns out George's data model was off, just a bit, in just a few places.  Oh, and the customer needed these few things added over there.  There was also this entity that marketing never mentioned, but now that they see things, they'll definitely need that.  "No problem," George says, "that's what these reviews are for, and we're happy to make those adjustments.  Our agile process allows us to do that with minimal longterm project impact."  George leaves the meeting refreshed and ready to tackle the next few weeks.

 

The 'Inbetween'

George returns to his desk ready to go.  The database changes are knocked out without a hiccup, and the update of the model from the database goes similarly smoothly.  There's a few weird things, and he forgets to delete a couple fields that moved around, but overall no major headaches.  Everything is looking good, the Silverlight client app is updated to accommodate the changed fields and everything is running smoothly... until he goes to add a child grid to maintain that new entity that was discovered.

 

The Story Falls Apart

"Of course", he thinks, "I've got to make sure I've generated the necessary methods on my service to handle the new entity.  What a silly thing to forget, no problem, the first time was a breeze, I'll just update things and get back to work."  Then George spends the next 30 minutes searching through Visual Studio to find the button/configuration file/etc that needs to be updated to trigger regeneration.  After 15 minutes, he's convinced he won't find anything, but he can't believe this particular use case wasn't handled out of the box.  After 30 minutes he gives up and then tries to decide how in the world he's going to maintain all the customizations that he has done while still generating the new code.  He eventually decides to just hand-code any updates that are necessary, using a couple of the other classes as templates.  The work is slow and tedious, but after a few attempts and some false starts, he manages to get everything working again.  In total, near half a day spent making updates that should have taken less than 1 hour of time.  He's back on track and moving forward, but he shutters to think about the next time he has to make changes, something virtually guaranteed by the agile processes he has used in the past so successfully...

 

Epilogue

So, what can we learn from George's story here?  I think the first take home is that the RIA services 'regeneration' story is not a good one.  Given the amount of code and logic behind the original generation process, it seems pretty silly to have left out any hooks to enable this process natively.  Visual Studio provides loads of extension points that automatically rebuild your services from a standard, configurable 'source' [the EF4 designer/code generation process is one such example].  The RIA Services 'Domain Service' wizard already requires that your solution compile, so why doesn't it produce something akin to the edmx files that EF4 uses which are then used to generate your services at compile time?

I, obviously, cannot answer that question, and I truly hope that Microsoft is working hard to improve the end user experience of RIA services to address some of these problems.  They have a site setup to handle 'feature requests' for RIA Services, and there are a few potential candidates that would make our lives easier:

And this isn't an exhaustive list, just a few that I picked out from the top vote getters.  This is a problem that needs to be solved, it's a giant pain to deal with now, but I'll walk through some steps that we've taken on projects to help alleviate the pain.

 

Alleviating the Pain

There are a few concrete steps that you can take to help alleviate the pain caused by the lack of regeneration capabilities.  Whether they end up being more or less work than hand coding will largely depend on the scope of the changes you need to make.  We use a combination of these methods and hand-coding to get the job done now.  There is anecdotal evidence that the methods below don't always work, and sometimes a restart of visual studio, or a full check-in/check-out cycle may be needed to trigger all the right things to happen.

Isolate custom metadata into a sub-directory, and use a 'single-class per file' approach

When regenerating, any existing metadata classes will NOT be regenerated, so putting them in their own directory allows easy 'Include/Exclude from Project' to force regeneration of all metadata.  Putting classes in their own file allows easy 'Include/Exclude' of a particular file to force metadata regeneration.  We usually use '<EntityName>.metadata.cs'.  Don't forget to put these classes in the 'Model' namespace, regardless of their location, so they match up with the EF4 generated classes.

Isolate 'extensions' to entities from metadata

When regenerating, it is necessary for your application to successfully compile.  While metadata updates do not generally result in compilation errors, extension updates generally do result in compilation errors.  So isolating these in their own file, which can be retained while also temporarily excluding metadata allows the regeneration process to proceed easily.  We usually use '<Entity>.extensions.cs'.  Don't forget to put these classes in the 'Model' namespace, regardless of their location, so they match up with the EF4 generated classes.

Isolate service extensions into their own file

When building RIA services, you invariably will want to add custom service methods to facilitate custom searches, loads, invokes, etc.  Even though the main DomainService derived class does NOT by default generate with the 'partial' modifier, it is simple to add and allows much greater 'regenability' by streamlining the process of regenerating the bulk of the service code.  We usually use '<ServiceName>DomainService.extensions.cs'.

'Regenerate' Code using existing Wizard

You can use the existing wizard to regenerate code, you just need to act as if you're creating a new service.  The steps we use are:

  1. Exclude any metadata classes that need to be regenerated [or the whole folder to get them all]
  2. Clean and Rebuild the solution.  This step is crucial, as it ensures that regeneration will function appropriately.
  3. Add a 'new' Domain Service to the project with the same name and a '.temp.cs' extension
  4. In the resulting dialog, clear the '.temp' from the service name and select the entities you would like to include in the service [yes, it's a pain to reselect them all every time].
  5. Copy the resulting output from the <ServiceName>DomainService.temp.cs file into the <ServiceName>DomainService.cs file, remembering to re-add the 'partial' modifier to the service class.
  6. Merge the resulting output from the <ServiceName>DomainService.temp.metadata.cs file into the classes in the 'Metadata' folder.
  7. Delete the two new .temp.cs files that were generated.
  8. 'Include' all files excluded in Step #1.
  9. Clean and rebuild.

The above steps accomplish a few things.  They allow us to retain any custom changes we've made.  They allow us to get the 'latest' version of the metadata and potentially save some typing in exchange for gratuitous cut-and-paste.  They allow source control branching/merging to work as desired, because we aren't deleting/re-adding files to the project that we care about [i.e. the .temp.cs files are never checked in, file changes are content based].  They keep the various concerns [core entities/service, metadata, extensions] separate and 'more maintainable' [to some folks].

 

Conclusion

As mentioned at the start, this story needs to be improved.  I'm hopefully that future versions of RIA services will improve on these areas, but in the mean time, we'll have to continue to find work arounds and 'best practices' that allow us to be productive using these tools and the agile processes that normally run our projects.  If you have additional techniques that you've used, things that I've missed, or just want to add your voice to the issue, please leave your comments below.