Using RIA DomainServices with ASP.NET and MVC 2
Posted
by Bobby Diaz
on Geeks with Blogs
See other posts from Geeks with Blogs
or by Bobby Diaz
Published on Sat, 03 Apr 2010 02:31:13 GMT
Indexed on
2010/04/03
9:43 UTC
Read the original article
Hit count: 761
Recently, I started working on a new ASP.NET MVC 2 project and I wanted to reuse the data access (LINQ to SQL) and business logic methods (WCF RIA Services) that had been developed for a previous project that used Silverlight for the front-end. I figured that I would be able to instantiate the various DomainService classes from within my controller’s action methods, because after all, the code for those services didn’t look very complicated. WRONG! I didn’t realize at first that some of the functionality is handled automatically by the framework when the domain services are hosted as WCF services. After some initial searching, I came across an invaluable post by Joe McBride, which described how to get RIA Service .svc files to work in an MVC 2 Web Application, and another by Brad Abrams. Unfortunately, Brad’s solution was for an earlier preview release of RIA Services and no longer works with the version that I am running (PDC Preview).
I have not tried the RC version of WCF RIA Services, so I am not sure if any of the issues I am having have been resolved, but I wanted to come up with a way to reuse the shared libraries so I wouldn’t have to write a non-RIA version that basically did the same thing. The classes I came up with work with the scenarios I have encountered so far, but I wanted to go ahead and post the code in case someone else is having the same trouble I had. Hopefully this will save you a few headaches!
1. Querying
When I first tried to use a DomainService class to perform a query inside one of my controller’s action methods, I got an error stating that “This DomainService has not been initialized.” To solve this issue, I created an extension method for all DomainServices that creates the required DomainServiceContext and passes it to the service’s Initialize() method. Here is the code for the extension method; notice that I am creating a sort of mock HttpContext for those cases when the service is running outside of IIS, such as during unit testing!
public static class ServiceExtensions
{
/// <summary>
/// Initializes the domain service by creating a new <see cref="DomainServiceContext"/>
/// and calling the base DomainService.Initialize(DomainServiceContext) method.
/// </summary>
/// <typeparam name="TService">The type of the service.</typeparam>
/// <param name="service">The service.</param>
/// <returns></returns>
public static TService Initialize<TService>(this TService service)
where TService : DomainService
{
var context = CreateDomainServiceContext();
service.Initialize(context);
return service;
}
private static DomainServiceContext CreateDomainServiceContext()
{
var provider = new ServiceProvider(new HttpContextWrapper(GetHttpContext()));
return new DomainServiceContext(provider, DomainOperationType.Query);
}
private static HttpContext GetHttpContext()
{
var context = HttpContext.Current;
#if DEBUG
// create a mock HttpContext to use during unit testing...
if ( context == null )
{
var writer = new StringWriter();
var request = new SimpleWorkerRequest("/", "/",
String.Empty, String.Empty, writer);
context = new HttpContext(request)
{
User = new GenericPrincipal(new GenericIdentity("debug"), null)
};
}
#endif
return context;
}
}
With that in place, I can use it almost as normally as my first attempt, except with a call to Initialize():
public ActionResult Index()
{
var service = new NorthwindService().Initialize();
var customers = service.GetCustomers();
return View(customers);
}
2. Insert / Update / Delete
Once I got the records showing up, I was trying to insert new records or update existing data when I ran into the next issue. I say issue because I wasn’t getting any kind of error, which made it a little difficult to track down. But once I realized that that the DataContext.SubmitChanges() method gets called automatically at the end of each domain service submit operation, I could start working on a way to mimic the behavior of a hosted domain service. What I came up with, was a base class called LinqToSqlRepository<T> that basically sits between your implementation and the default LinqToSqlDomainService<T> class.
[EnableClientAccess()]
public class NorthwindService : LinqToSqlRepository<NorthwindDataContext>
{
public IQueryable<Customer> GetCustomers()
{
return this.DataContext.Customers;
}
public void InsertCustomer(Customer customer)
{
this.DataContext.Customers.InsertOnSubmit(customer);
}
public void UpdateCustomer(Customer currentCustomer)
{
this.DataContext.Customers.TryAttach(currentCustomer,
this.ChangeSet.GetOriginal(currentCustomer));
}
public void DeleteCustomer(Customer customer)
{
this.DataContext.Customers.TryAttach(customer);
this.DataContext.Customers.DeleteOnSubmit(customer);
}
}
Notice the new base class name (just change LinqToSqlDomainService to LinqToSqlRepository). I also added a couple of DataContext (for Table<T>) extension methods called TryAttach that will check to see if the supplied entity is already attached before attempting to attach it, which would cause an error!
3. LinqToSqlRepository<T>
Below is the code for the LinqToSqlRepository class. The comments are pretty self explanatory, but be aware of the [IgnoreOperation] attributes on the generic repository methods, which ensures that they will be ignored by the code generator and not available in the Silverlight client application.
/// <summary>
/// Provides generic repository methods on top of the standard
/// <see cref="LinqToSqlDomainService<TContext>"/> functionality.
/// </summary>
/// <typeparam name="TContext">The type of the context.</typeparam>
public abstract class LinqToSqlRepository<TContext> : LinqToSqlDomainService<TContext>
where TContext : System.Data.Linq.DataContext, new()
{
/// <summary>
/// Retrieves an instance of an entity using it's unique identifier.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="keyValues">The key values.</param>
/// <returns></returns>
[IgnoreOperation]
public virtual TEntity GetById<TEntity>(params object[] keyValues) where TEntity : class
{
var table = this.DataContext.GetTable<TEntity>();
var mapping = this.DataContext.Mapping.GetTable(typeof(TEntity));
var keys = mapping.RowType.IdentityMembers
.Select((m, i) => m.Name + " = @" + i)
.ToArray();
return table.Where(String.Join(" && ", keys), keyValues).FirstOrDefault();
}
/// <summary>
/// Creates a new query that can be executed to retrieve a collection
/// of entities from the <see cref="DataContext"/>.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <returns></returns>
[IgnoreOperation]
public virtual IQueryable<TEntity> GetEntityQuery<TEntity>() where TEntity : class
{
return this.DataContext.GetTable<TEntity>();
}
/// <summary>
/// Inserts the specified entity.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="entity">The entity.</param>
/// <returns></returns>
[IgnoreOperation]
public virtual bool Insert<TEntity>(TEntity entity) where TEntity : class
{
//var table = this.DataContext.GetTable<TEntity>();
//table.InsertOnSubmit(entity);
return this.Submit(entity, null, DomainOperation.Insert);
}
/// <summary>
/// Updates the specified entity.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="entity">The entity.</param>
/// <returns></returns>
[IgnoreOperation]
public virtual bool Update<TEntity>(TEntity entity) where TEntity : class
{
return this.Update(entity, null);
}
/// <summary>
/// Updates the specified entity.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="entity">The entity.</param>
/// <param name="original">The original.</param>
/// <returns></returns>
[IgnoreOperation]
public virtual bool Update<TEntity>(TEntity entity, TEntity original)
where TEntity : class
{
if ( original == null )
{
original = GetOriginal(entity);
}
var table = this.DataContext.GetTable<TEntity>();
table.TryAttach(entity, original);
return this.Submit(entity, original, DomainOperation.Update);
}
/// <summary>
/// Deletes the specified entity.
/// </summary>
/// <typeparam name="TEntity">The type of the entity.</typeparam>
/// <param name="entity">The entity.</param>
/// <returns></returns>
[IgnoreOperation]
public virtual bool Delete<TEntity>(TEntity entity) where TEntity : class
{
//var table = this.DataContext.GetTable<TEntity>();
//table.TryAttach(entity);
//table.DeleteOnSubmit(entity);
return this.Submit(entity, null, DomainOperation.Delete);
}
protected virtual bool Submit(Object entity, Object original, DomainOperation operation)
{
var entry = new ChangeSetEntry(0, entity, original, operation);
var changes = new ChangeSet(new ChangeSetEntry[] { entry });
return base.Submit(changes);
}
private TEntity GetOriginal<TEntity>(TEntity entity) where TEntity : class
{
var context = CreateDataContext();
var table = context.GetTable<TEntity>();
return table.FirstOrDefault(e => e == entity);
}
}
4. Conclusion
So there you have it, a fully functional Repository implementation for your RIA Domain Services that can be consumed by your ASP.NET and MVC applications. I have uploaded the source code along with unit tests and a sample web application that queries the Customers table from inside a Controller, as well as a Silverlight usage example.
As always, I welcome any comments or suggestions on the approach I have taken. If there is enough interest, I plan on contacting Colin Blair or maybe even the man himself, Brad Abrams, to see if this is something worthy of inclusion in the WCF RIA Services Contrib project. What do you think?
Enjoy!
© Geeks with Blogs or respective owner