Server Side Processing in ADO.NET/WCF Data Services

ADO.NET Data Services provide a very simple way for the servers to publish data. However, it lacks of flexibility in terms of processing the data before forwarding it to the clients. In this paper, I show a simple approach that provides business logic processing before forwarding data to the clients.

Introduction

The world of web services encounters another trend by REST-full services after the invention of SOAP based protocols. The acronym REST stands for Representational State Transfer; this basically means that each unique URL is a reference to a resource. The main advantage of REST services is their simplicity compare to the SOAP based services. REST services use the tradition HTTP methods to do the basic CRUD operations. For example, the HTTP GET method is used to address an object or a resource, whist the DELETE HTTP method is used to delete a resource on the server. The POST and PUT HTTP methods are used to update or create data on the server too. REST services can be used by a simple web browser without any special knowledge about protocols or any special tool and it makes them available to a variety of different clients and nowadays using REST services is a robust approach for publishing data on the web.

Microsoft makes a big stride in developing REST services by introducing ADO.net Services. Known as WCF Data Services in the Framework 4.0, they allow a very simple way to expose database tables as REST-full services. Data Services can be published on a Web Server or any other WCF Host environment and in one word; there is a bunch of different hosting options. The process of creating them is very simple too. At first, one should create an Entity Framework and then maps the tables to conceptual classes in the entity framework. Secondly, he/she should create a Data Service and finally, configuring the Entity Framework in the InitilizeService method of the data service. Pretty simple! Isn’t it?

On the client side, a .NET Framework client can get advantages of System.Data.Services.Client namespace classes to access data services. The heart class of the namespace is DataServiceContext. It is similar to the DataContext class of the LINQ to SQL technology. According to the MSDN “The DataServiceContext represents the runtime context of an ADO.NET data service. ADO.NET Data Services are stateless, but the DataServiceContext is not. State on the client is maintained between interactions in order to support features such as update management. This class, and the DataServiceQuery class that represents a particular HTTP request to a data service, are the two main classes in the client library.

Developing a three tier application using the data services has clear advantages such as simplicity, RAD development, less code, easy maintenance a few to mention. But it has some limitations too. The main advantage of three tier applications is the ability to do some processing and business logic in the middle tier, something that data services have poor support on it.

Focus of the Paper

The focus of this paper is providing an approach that allows developers to do some processing in the server side before forwarding the data to the clients. The proposed method can be used easily and it doesn’t impose too complexity. Here is some characteristic of the approach:

  • It uses ReflectionProvider and LINQ to SQL framework to create data services.
  • It doesn’t complicate the process of creating data services.
  • By Wrapping LINQ to SQL Tables<T>, it provides a QueryCompleted event that allows developer to have access to the queried data before forwarding it to the clients.

In order to simplify my approach and the reason of its existence, I will describe some validation features of Data Services plus the different providers of data services in .NET framework. After that, I will delve into the proposed approach.

Data Services Providers

There are three types of providers in the .NET framework for creating Data Services. The main one is Entity Framework provider. In the first line of a DataService,

public class WebDataService1 : DataService< /* TODO: put your data source class name here */ >

if one uses an ObjectContext in the commented TODO part, then the DataService is based on Entity Framework Provider. Entity Framework Provider uses the ADO.NET Entity Framework to enable you to use relational data with a data service by defining a data model that maps to relational data [MSDN]. Using the Entity Framework Provider is simpler than the other providers. The other option here is using Reflection Provider. Reflection provider uses reflection to enable you to define a data model based on existing data classes that can be exposed as instances of the IQueryable interface. Updates are enabled by implementing theIUpdatable interface. You should use this provider when you have static data classes that are defined at runtime, such as those generated by LINQ to SQL or defined by a typed DataSet [MSDN]. The last option is creating a custom provider. There are a bunch of interfaces that should be implemented in order to have a custom provider. Such providers are very flexible, but indeed creating them is not that easy. An interested reader can find a full functional implementation here.

Interceptors

Interceptors are .NET attributes that provide a flexible way to do entity level validation and authorization in the ADO.NET data services. QueryInterceptorAttribute is used to do query authorization and validation whilst ChangeInterceptor is used to do change authorization and validation. Here is an example on how to use QueryInterceptorAttribute.

[QueryInterceptor("Orders")]

public Expression<Func<Order, bool>> FilterOrders()

{

return o => o.Customer.Name == "test";

}

Methods of a data service that decorated with the QueryInterceptorAttribute must have no parameters and their return type must be Expression<Func<Order, bool>>. Theses functions are used to validate and authorize the client access to resources. The above method only allows queries on the “Orders” IQueryable that their Customer.Name is “test”. Such technique for entity-base validation is quite flexible, but it doesn’t provide a way for server side processing before forwarding the data to the client. The other shortcoming of Interceptors is the fact that they do not provide field base authorization, they only provide entity base validation. Sometimes we need field base authorization. For example, consider a situation that all users have access to the “Orders”, but only admin users have right to see some fields of the “Orders”. These shortcomings can be covered using server side processing.

My Approach

A data service can publish the data of any class that implements IQueryable<T> interface. IQueryable<T> Provides functionality to evaluate queries against a specific data source wherein the type of the data is known. Table<T> class of LINQ to SQL framework and ObjectQuery<TEntity> of Entity Framework are examples of classes that implement IQueryable<T> interface against a database. The signature of the interface is as follows:

public interface IQueryable<T> : IEnumerable<T>, IQueryable, IEnumerable

{

}

Where the generic IQueryable is as follows:

public interface IQueryable : IEnumerable

{

Type ElementType { get; }

Expression Expression { get; }

IQueryProvider Provider { get; }

}

The main property of the IQueryable is its Provider. The IQueryProvider Defines methods to create and execute queries that are described by an IQueryable object. Implementing an IQueryable plus its provider to translate LINQ Expressions to SQL queries is not an easy task at all. Interested user can find a complete example on how to implement an IQueryProvider here.

My idea about enabling business process in the Data Services is based on wrapping the existing Table instances of the LINQ to SQL framework by a class in such a way that the wrapper class provides a QueryCompleted event. The mentioned event has access to the returned data, so that a developer can process the queried data once it has been ready. Here is the class diagram of the proposed approach.

There are two main classes here. The BQueryable implements IQueryable and wraps Table<Entity> and BQueryProvider implements IQueryProvider. Here is the code of BQueryProvider.

public class BQueryProvider<T> : IQueryProvider where T : class

{

#region Constructor

public BQueryProvider(Table<T> t)

{

table = t;

}

#endregion

#region Private members

private Table<T> table;

#endregion

#region IQueryable implementation

IQueryable IQueryProvider.CreateQuery(Expression expression)

{

IQueryable<T> result = (table as IQueryProvider).CreateQuery<T>(expression);

onQueryCompleted(result);

return result;

}

IQueryable<TElement> IQueryProvider.CreateQuery<TElement>(Expression expression)

{

IQueryable<T> result = (table as IQueryProvider).CreateQuery<T>(expression);

onQueryCompleted(result);

return result as IQueryable<TElement>;

}

object IQueryProvider.Execute(Expression expression)

{

object value = (table as IQueryProvider).Execute(expression);

return value;

}

TResult IQueryProvider.Execute<TResult>(Expression expression)

{

return (table as IQueryProvider).Execute<TResult>(expression);

}

#endregion

#region Events

public event EventHandler<QueryResultEventArgs<IQueryable<T>>> QueryCompleted;

protected void onQueryCompleted(IQueryable<T> t)

{

if (QueryCompleted != null)

{

QueryCompleted(this, new QueryResultEventArgs<IQueryable<T>>(t));

}

}

#endregion

}

The class doesn’t implement an IQueryProvider from the scratch; instead it wraps the existing one of the LINQ to SQL framework. Using this approach, the risk of using a third party implementation of the IQueryProvider has been reduced. The class has a QueryCompleted event that is fired in the CreateQuery methods, before returning the data. This event enables the BQueryable class to have access to the returned data. Here is the code of the BQueryable class.

public class BQuerable<T> : IQueryable<T>, IQueryable, ITable where T : class

{

#region Constructor

public BQuerable(Table<T> t)

{

table = t;

queryProvider = new BQueryProvider<T>(t);

queryProvider.QueryCompleted += new EventHandler<QueryResultEventArgs<IQueryable<T>>>(queryProvider_QueryCompleted);

}

#endregion

#region Events

private void queryProvider_QueryCompleted(object sender, QueryResultEventArgs<IQueryable<T>> e)

{

if (QueryCompleted != null)

{

QueryCompleted(this, new QueryResultEventArgs<IQueryable<T>>(e.Result));

}

}

protected void onEnumeratorReturned(IEnumerator enumerator)

{

if (QueryCompleted != null)

{

QueryCompleted(this, new QueryResultEventArgs<IQueryable<T>>(this));

}

}

public event EventHandler<QueryResultEventArgs<IQueryable<T>>> QueryCompleted;

#endregion

#region private members

private Table<T> table;

BQueryProvider<T> queryProvider;

#endregion

#region IEnumerable

IEnumerator IEnumerable.GetEnumerator()

{

IEnumerator enumerator = table.GetEnumerator();

onEnumeratorReturned(enumerator);

return enumerator;

}

IEnumerator<T> IEnumerable<T>.GetEnumerator()

{

IEnumerator<T> result = table.GetEnumerator();

onEnumeratorReturned(result);

return result;

}

#endregion

#region ITable

DataContext ITable.Context

{

get

{

return table.Context;

}

}

bool ITable.IsReadOnly

{

get

{

return table.IsReadOnly;

}

}

void ITable.Attach(object entity)

{

table.Attach(entity as T);

}

void ITable.Attach(object entity, bool asModified)

{

table.Attach(entity as T, asModified);

}

void ITable.Attach(object entity, object original)

{

table.Attach(entity as T, original as T);

}

void ITable.AttachAll(IEnumerable entities)

{

table.AttachAll(entities.Cast<T>());

}

void ITable.AttachAll(IEnumerable entities, bool asModified)

{

table.AttachAll(entities.Cast<T>(), asModified);

}

void ITable.DeleteAllOnSubmit(IEnumerable entities)

{

table.DeleteAllOnSubmit(entities.Cast<T>());

}

void ITable.DeleteOnSubmit(object entity)

{

table.DeleteOnSubmit(entity as T);

}

ModifiedMemberInfo[] ITable.GetModifiedMembers(object entity)

{

return table.GetModifiedMembers(entity as T);

}

object ITable.GetOriginalEntityState(object entity)

{

return table.GetOriginalEntityState(entity as T);

}

void ITable.InsertAllOnSubmit(IEnumerable entities)

{

table.InsertAllOnSubmit(entities.Cast<T>());

}

void ITable.InsertOnSubmit(object entity)

{

table.InsertOnSubmit(entity as T);

}

#endregion

#region IQueryable

Type IQueryable.ElementType

{

get

{

return (table as IQueryable).ElementType;

}

}

Expression IQueryable.Expression

{

get

{

return (table as IQueryable).Expression;

}

}

IQueryProvider IQueryable.Provider

{

get

{

return queryProvider;

}

}

#endregion

}

Similar to the BQueryProvider, the BQueryable class has a QueryCompleted event too. This event is fired, once the BQueryProvider fires its own QueryCompleted event. Developers should listen to this event in order to have access to the returned data.

Using the Code

In order to use the code, a developer should create a LINQ to SQL DataContext. The following capture is the one of the attached demo project.

Next, developer should create a new class that inherits from the BusinessDataContext class. In the new class, developer should specify the Tables<T> that he/she wants to expose via the DataService by wrapping them using BQueryable class. He/she can listen to their QueryCompleted events too. Here is the code in the attached demo project.

[DataServiceKeyAttribute("ProductID")]

public partial class Product { }

[DataServiceKeyAttribute("ProductModelID")]

public partial class ProductModel { }

public class CustomDataContext : BusinessDataContext<AdventureDataContext>

{

public CustomDataContext()

{

DataContext = new AdventureDataContext();

Products = new BQuerable<Product>(DataContext.Products);

ProductModels = new BQuerable<ProductModel>(DataContext.ProductModels);

Products.QueryCompleted += new EventHandler<QueryResultEventArgs<IQueryable<Product>>>(Products_QueryCompleted);

ProductModels.QueryCompleted += new EventHandler<QueryResultEventArgs<IQueryable<ProductModel>>>(ProductModels_QueryCompleted);

}

void ProductModels_QueryCompleted(object sender, QueryResultEventArgs<IQueryable<ProductModel>> e)

{

}

void Products_QueryCompleted(object sender, QueryResultEventArgs<IQueryable<Product>> e)

{

foreach (Product product in e.Result)

{

product.Name += " has been processed";

}

}

public BQuerable<Product> Products

{

get

{

return products;

}

set

{

products = value;

}

}

private BQuerable<Product> products;

public BQuerable<ProductModel> ProductModels { get; set; }

}


And now, the created data context is ready to feed the DataService. Here is the code of the DataService.

[System.ServiceModel.ServiceBehavior(IncludeExceptionDetailInFaults = true)]

public class DataService : DataService<CustomDataContext>

{

// This method is called only once to initialize service-wide policies.

public static void InitializeService(IDataServiceConfiguration config)

{

try

{

config.SetEntitySetAccessRule("ProductModels", EntitySetRights.AllRead);

config.SetEntitySetAccessRule("Products", EntitySetRights.AllRead);

}

catch (Exception ex)

{

}

}

protected override void OnStartProcessingRequest(ProcessRequestArgs args)

{

base.OnStartProcessingRequest(args);

}

protected override void HandleException(HandleExceptionArgs e)

{

try

{

e.UseVerboseErrors = true;

}

catch (Exception ex)

{

Console.WriteLine(ex.Message);

}

}

}

The code can be downloaded here. The zip file contains Client application, Server application and the database. You need SQL EXPRESS 2005 and Visual Studio 2008 SP1 to run the code. The zip file also includes the scripts of the database too.

By Siyamand Ayubi   Popularity  (5122 Views)