ADO.NET Data Service provides a fantastic service for consuming data from SilverLight.
Unfortunately, there is a fly in the ointment when it comes to consuming data across multiple domains. The problem is that the the implementation of ADO.NET Data Services (Astoria) doesn’t work cross domain from SilverLight, even if there is a clientaccesspolicy.xml file. Apparently the network stack that it uses is not the normal Silverlight one.(See Pablo Castro’s blog for details)
This is a bummer, as now instead of having a great rich client object model around your database you are back in the business of having to create a proxy service implementation.
This blog post shows a very easy way to work around this problem by create a proxy service which is fully functional object model with very little code.
Let’s start with an hypothetical project:
Step 1- Add Service Reference to the Database Web Service
Add a service reference for http://foo.com/FooStore.svc to your web service http://myapp.com. Visual Studio will create a DataServiceContext class around that service and the entities it provides.
This gives your application a full blown LINQ based web service with the ability to consume and interact with the database on foo.com. Cool!
Step 2 – Create new proxy service on the Application Web Service
Create a new ADO.NET Data Service on http://myapp.com called FooStoreProxy.svc. This will be the service that your silverlight application will consume, which you want to look exactly like FooStore.svc but be on your domain.
Step 3 – Wrap database service and override CreateDataSource()
Now you need to simply use the Astoria model which was generated in step 1 as the entity model that the new proxy service is wrapped around. It will take anything that returns IQueryable as objects in the new service, and we have a brand new shiny one which we created in step 1.
You will probably need to override the CreateDataSource() method so you can pass your connection string to the ADO.NET Data Service you are consuming like this:
public class FooStoreProxy : DataService< FooStore> { // This method is called only once to initialize service-wide policies. public static void InitializeService(IDataServiceConfiguration config) { // TODO: set rules to indicate which entity sets and service operations are // visible, updatable, etc. // Examples: } protected override FooStore CreateDataSource() { ConnectionStringsSection connectionStringsSection = WebConfigurationManager.GetSection("connectionStrings") as ConnectionStringsSection; string connectionName = "default"; return new FooStore(connectionStringsSection.ConnectionStrings[connectionName].ConnectionString); } }
Step 4 – Add Service Reference to FooStoreProxy.svc in your Silverlight Application
Configure your silverlight app by adding a service reference to http://myapp.com/FooStoreProxy.svc This will auto-generate a client wrapper around the proxy service.
You now should be able to use the object model generated in your silverlight application to query the http://myapp.com/FooStoreProxy.svc service, which will then query the real http://foo.com/FooStore.svc service.
If all you want to do is to have read only access you are done. But if you want to be able to do CRUD operations then you have a couple more things will need to do.
Step 5 – implement IUpdateable on the application web service
The reason you can’t update your service is because the class doesn’t implement IUpdateable. Luckily the generated class is a partial class, so you can just create a partial class file which adds the needed interface.
Here’s code which shows a complete implementation of IUpdateable which should be generic and work in most cases:
// Add IUpdateable to our ADO.NET Data Services generated DataServiceContext wrapper public partial class FooStoreProxy : IUpdatable { #region IUpdatable Members public void AddReferenceToCollection(object targetResource, string propertyName, object resourceToBeAdded) { base.AddLink(targetResource, propertyName, resourceToBeAdded); } public void ClearChanges() { // clear out links foreach (var link in base.Links) base.DetachLink(link.Source, link.SourceProperty, link.Target); // clear out entities foreach (var entity in base.Entities) base.Detach(entity.Entity); } public object CreateResource(string containerName, string fullTypeName) { object obj = Activator.CreateInstance(this.GetType().Assembly.GetType(fullTypeName)); AddObject(containerName, obj); return obj; } public void DeleteResource(object targetResource) { DeleteObject(targetResource); } public object GetResource(IQueryable query, string fullTypeName) { object resource = query.Cast<object>().SingleOrDefault(); // fullTypeName can be null for deletes if (fullTypeName != null && resource.GetType().FullName != fullTypeName) throw new Exception("Unexpected type for resource"); return resource; } public object GetValue(object targetResource, string propertyName) { Type type = targetResource.GetType(); PropertyInfo propInfo = type.GetProperty(propertyName); if (propInfo == null) throw new Exception("Can't find property"); object val = propInfo.GetValue(targetResource, null); return val; } public void RemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved) { base.DeleteLink(targetResource, propertyName, resourceToBeRemoved); } public object ResetResource(object resource) { Detach(resource); return resource; } public object ResolveResource(object resource) { // just reflect the object back, since that's what we // gave them in CreateResource return resource; } public void SaveChanges() { base.SaveChanges(); } public void SetReference(object targetResource, string propertyName, object propertyValue) { base.SetLink(targetResource, propertyName, propertyValue); } public void SetValue(object targetResource, string propertyName, object propertyValue) { PropertyInfo propInfo = targetResource.GetType().GetProperty(propertyName); if (propInfo == null) throw new Exception("Can't find property"); propInfo.SetValue(targetResource, propertyValue, null); } #endregion } }
You should now be able to do CRUD operations against your store!
Step 5 – Add IExpandProvider implementation to the application web service
There is one thing left which does not work quite yet. ADO.NET Data Services has a special LINQ command called Expand(). This lets you control the shape of the results which come back to reduce roundtrips. To make the proxy handle Expand correctly you need to implement IExpandProvider interface on FooStoreProxy. Luckily, while the documentation is thin the implementation is quite straight forward.
Here is an implementation which you will need to replace only the switch block with each entity that is exposed by your service:
/// /// IExpandProvider /// implements the $expand syntax by simply passing through to the proxied ADO.NET Data Service /// #region IExpandProvider Members public IEnumerable ApplyExpansions(IQueryable queryable, ICollection expandPaths) { string expand = ""; foreach (var expandSegmentCollection in expandPaths) { string segment = ""; foreach (var expandSegment in expandSegmentCollection) { segment += String.Format("{0}{1}", (segment.Length > 0) ? "/" : "", expandSegment.Name); } // whatever entity type this query is using needs to to use a // strongly typed DataServiceQuery object to get access to the AddQueryOption...bummer switch (queryable.ElementType.Name) { case "Item": return ((DataServiceQuery<Item>)queryable).Expand(segment); case "Person": return ((DataServiceQuery<Person>)queryable).Expand(segment); // add/remove entries here for each item type that is queried as a result set. // NOTE: The switch statement and DataServiceQuery<ElementType.Name> must match } } // return massaged query return queryable; } #endregion
And there you go. This is relatively easy to use and seems to work just great. I would love to hear from others if this works for them too!
-Tom