Your boss suddenly decides that Silverlight is the "next big thing" and asks you to begin converting your line of business application to use the new features. Being a solid architect, of course, you have already built a nice application that is structured in layers. You have a data access layer that abstracts persistence behind interfaces, a domain layer that describes business models, a business logic layer for heavy lifting, a services layer for interconnectivity, and a presentation layer that is ASP.NET or something similar.
RIA services looks promising, but there is one problem. You see that you can create a LINQ to SQL based service, or an Entity Framework service, but you're not comfortable going to production with either of those because then it would introduce an entirely new way of accessing data. You want to keep your data access layer (maybe it's built using Enterprise Library or NHibernate or something similar).
What do you do?
Fortunately, RIA services is not limited to LINQ or EF. You can build a domain service that handles your POCO (plain-ole' CLR objects) domain models. If you are willing to extend your classes a bit and haven't already begun decorating your POCO objects with data annotations, you can handle that, too! Here's how.
Let's assume I've got a decent entity model based on entities that have an integer for their identifier. I do like INotifyPropertyChanged
but had reservations about decorating my model with data annotations or validation attributes. Just didn't seem right. So what I have is a base entity that looks like this:
public abstract class BaseEntity : INotifyPropertyChanged { private bool _isDirty = false; private bool _isNew = true; private int _id = -1; public virtual int ID { get { return _id; } set { if (!value.Equals(_id)) { _id = value; OnPropertyChanged("ID", true); if (_isNew && value > 0) { _isNew = false; OnPropertyChanged("IsNew", true); } } } } public virtual bool IsNew { get { return _isNew; } } public virtual bool IsDirty { get { return _isDirty; } } public void Reset() { if (_isDirty) { _isDirty = false; OnPropertyChanged("IsDirty", false); } } protected void OnPropertyChanged(string property) { OnPropertyChanged(property, true); } private void OnPropertyChanged(string property, bool setDirtyFlag) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(property)); } if (setDirtyFlag) { if (!_isDirty) { _isDirty = true; if (handler != null) { handler(this, new PropertyChangedEventArgs("IsDirty")); } } } } #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; #endregion protected abstract bool _Equals(object obj); public override bool Equals(object obj) { return _Equals(obj); } public override int GetHashCode() { return _id.GetHashCode(); } }
I have a user entity defined as well. This contains properties for a username, a first and last name, and an email address. Only the username is required. The entity looks like this:
public class UserEntity : BaseEntity { private string _userName, _firstName, _lastName, _email; public virtual string UserName { get { return _userName; } set { if (value == null || !value.Equals(_userName)) { _userName = value; OnPropertyChanged("UserName"); } } } public virtual string FirstName { get { return _firstName; } set { if (value == null || !value.Equals(_firstName)) { _firstName = value; OnPropertyChanged("FirstName"); } } } public virtual string LastName { get { return _lastName; } set { if (value == null || !value.Equals(_lastName)) { _lastName = value; OnPropertyChanged("LastName"); } } } public virtual string Email { get { return _email; } set { if (value == null || !value.Equals(_email)) { _email = value; OnPropertyChanged("Email"); } } } protected override bool _Equals(object obj) { return obj is UserEntity && ((UserEntity)obj).Equals(ID); } }
Note: it's quite possible that you didn't declare your properties as virtual. While I won't make you rewrite your domain model layer, it would be helpful if you're willing to make that one change ... I'll explain why later. Virtual gives us some flexibility to extend later on.
Now for my data access class, I have an interface that defines my basic data handling needs. How I implement these could be anything from NHibernate to Enterprise Library to my own SQL provider that directly accesses the database and parses readers and a data sets into my POCO objects. The point is, I can do this all through a clean interface, like this:
public interface IDataHandler<TEntity> where TEntity : BaseEntity { TEntity Load(int id); void Delete(int id); int Save(TEntity entity); IEnumerable<TEntity> List(); }
Great. So now I can implement and extend as needed. For example, I might create a concrete UserDataHandler
that is based on the IDataHandler<UserEntity>
interface. Sound good?
Now we want to build our domain service in RIA but maintain backwards compatibility with our existing system. After all, a large line of business application wasn't written overnight. It could take months to convert all of those screens to Silverlight and we might not even touch them all. It's important to maintain a consistent data access layer so I can fix things in one place and still reuse the existing connectors in my legacy code.
The first thing I'll do is fire up a new Silverlight Business Project. This puts in some plumbing that I might use, or I might throw out, but more importantly it integrates the structure for my Silverlight project to be able to seamlessly communicate with my RIA services.
It makes sense for those of you new to RIA to take a step back and mention two things.
First, RIA is "magic" and exposes a neat class that I can work with, without having to deal with the nuances of WCF services. In the end, however, RIA generates WCF endpoints. These are 100% bona-fide WCF service points that anything can plug into ... I'm just going to do it with my Silverlight application for now.
Second, RIA is "magic" and performs some code generation. I don't see these in a special designer.cs
file, but it's there. Code generation isn't a popular term because of some bad projects in the past, so I think the cool word for it now is "projection." We say RIA projects code to my Silverlight application. This means I'll have access to the host web namespace from my Silverlight application.
Let's dig in. What I want to do is create a Domain Service that allows me to reuse my existing data access layer and POCO classes. What I'll do is add a new item to my web application (the one that's hosting the Silverlight, not the Silverlight application itself) and I'll call it LOBDomainService (line of business domain service). The shell that is provided for me looks like this:
[EnableClientAccess()] public class LOBDomainService : DomainService { }
This is the start of a workable service. Because I called my class LOBDomainService, RIA will project for me a LOBDomainContext in the Silverlight client. We'll get to that in a moment.
First, we need to get our data access layer into the domain service. There are several ways to do this:
Factory
IDataHandler<UserEntity> dataHandler = MyDataFactory.GetDataAccess<UserEntity>();
Constructor Injection using something like StructureMap or Unity
private IDataHandler<UserEntity> _dataHandler; public LOBDomainService(IDataHandler<UserEntity> dataHandler) { _dataHandler = dataHandler; }
Managed Extensibility Framework (MEF)
[Import] IDataHandler<UserEntity> DataHandler { get; set; }
... or maybe you just want to new it up. It's up to you.
At this point, however, we have a problem. While I'm able to import my data handler and my objects, the RIA framework doesn't know much about my entities. This is because it depends on data annotations to get hints. With data annotations, I can specify which field is the key field for the entity. I can provide user friendly column names, hints, and descriptions, flag which fields are required and even provide validation.
Some shops may already use these annotations, as they are encapsulated in System.ComponentModel.DataAnnotations
and are database and data-strategy independent. However, if you either haven't used these, or simply don't want to "dirty" your base domain models, there is a way to "cheat" a bit and get this to work.
Remember how I mentioned using virtual
on your properties would come into play? Let's create a new class called UserEntityExtension
. I'll go ahead and base it on my POCO class, but add some annotations.
public class UserEntityExtension : UserEntity { public UserEntityExtension() { } public UserEntityExtension(UserEntity baseEntity) { if (baseEntity.ID > 0) { ID = baseEntity.ID; } UserName = baseEntity.UserName; FirstName = baseEntity.FirstName; LastName = baseEntity.LastName; Email = baseEntity.Email; Reset(); } [Key] [ReadOnly(true)] [Display(AutoGenerateField=false)] public override int ID { get { return base.ID; } set { base.ID = value; } } [Display(Name="Username")] [Required] [RegularExpression("^[A-Za-z0-9]+$", ErrorMessage="Please enter a valid user name using only alphanumeric characters without spaces.")] public override string UserName { get { return base.UserName; } set { base.UserName = value; } } [Display(Name="First Name")] public override string FirstName { get { return base.FirstName; } set { base.FirstName = value; } } [Display(Name="Last Name")] public override string LastName { get { return base.LastName; } set { base.LastName = value; } } [Display(Name="Email Address")] [RegularExpression(@"^([\w-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$", ErrorMessage="Please enter a valid email address.")] public override string Email { get { return base.Email; } set { base.Email = value; } } [Display(AutoGenerateField=false)] public override bool IsNew { get { return base.IsNew; } } [Display(AutoGenerateField=false)] public override bool IsDirty { get { return base.IsDirty; } } }
As you can see, I'm simply passing through to the base properties, but annotating these with various tags. The constructor takes the base class and populates itself, making it easy to convert from the base type to the extended type. Now I'm ready to complete my domain service model:
[EnableClientAccess()] public class LOBDomainService : DomainService { public LOBDomainService() { // MEF set up goes here } [Import(AllowRecomposition=true)] IDataHandler<UserEntity> UserContext { get; set; } public IQueryable<UserEntityExtension> GetUsers() { return UserContext.List().ToList().ConvertAll(u => new UserEntityExtension(u)).AsQueryable(); } public void InsertUser(UserEntityExtension entity) { UserContext.Save(entity); } public void UpdateUser(UserEntityExtension entity) { UserContext.Save(entity); } public void DeleteUser(UserEntityExtension entity) { UserContext.Delete(entity.ID); } }
The Liskov Substitution Principle (LSP) states that "if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program." In this case, we can safely pass the extended class to our data handler that only knows about the base class without worry.
On the Silverlight side, I can now wire up a fast grid and form to see the results of my hard work. I'll sneak into the Views/Home.xaml
template and add the following references:
xmlns:ria="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Ria" xmlns:riaData="clr-namespace:System.Windows.Data;assembly=System.Windows.Controls.Ria" xmlns:local="clr-namespace:LOBApp.Web.Services" xmlns:dataForm="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data.DataForm.Toolkit" xmlns:dataGrid="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
Then, I'll wire in the data source, a grid, and a data form, like this:
<ria:DomainDataSource AutoLoad="True" Name="userSource" QueryName="GetUsers"> <ria:DomainDataSource.DomainContext> <local:LOBDomainContext/> </ria:DomainDataSource.DomainContext> </ria:DomainDataSource> <dataGrid:DataGrid RowDetailsVisibilityMode="VisibleWhenSelected" SelectionMode="Single" AutoGenerateColumns="True" ItemsSource="{Binding Path=Data, ElementName=userSource}"> <dataGrid:DataGrid.RowDetailsTemplate> <DataTemplate> <dataForm:DataForm x:Name="dataForm" AutoGenerateFields="True" CurrentItem="{Binding Path=DataContext, ElementName=dataForm}"/> </DataTemplate> </dataGrid:DataGrid.RowDetailsTemplate> </dataGrid:DataGrid>
With just that little bit of code, I can compile and generate and get something like this (notice it takes on the field names as well as validations, etc):
Obviously there's much more to do (we haven't touched security or made a submit button or added delete functionality, for example) but this should give you an idea of not only how powerful RIA services truly are, but also how there is plenty in place to enable you to leverage your existing architecture with the new model.