Wednesday 4 April 2012

Configurable OData Service - Part 3 – Custom Data Service Provider

In this post I am going to delve into the implementation of the Custom Data Service Provider that will unify the Type metadata, Dynamic Type generation and WCF OData services.
For brevity of this post I am not going to cover all of the classes required for the implementation of the Custom Data Service Provider just those that are key to this implementation. Implementing a Custom Data Service Provider is actually  a relatively straight forward task once you get your head around the interfaces involved. The details behind the steps required in Creating a Data Service Provider has been covered in a series of excellent posts by Alex D James on his blog. I have based my implementation on some of the concepts expressed in those posts.

Class Model Overview
ClassDiagram1ClassDiagram2

There are a number of classes required to expose the underlying custom data source as a service. I will discuss each one in turn to give a simple overview.

DynamicDataService is an abstract base class that derives from DataService<DynamicDataContext> and the IServiceProvider interface. The purpose of this class is to and allow the WCF OData service infrastructure to instantiate the various custom data service provider types via the object GetService(Type serviceType) method of the IServiceProvider interface.

DynamicDataContext is the class that represents the underlying data source that will be exposed via an OData service. It derives from DataContext and the IDynamicDataContext interface.

The IDynamicDataContext interface is a custom interface to abstract the implementation so that the classes of the custom data provider can be unit tested. It derives from DataContext because it is a LINQ to SQL based data source, I will discuss this class in more detail later in this post.

DomainModelService inherits from the abstract base class DynamicDataService. It implements the two abstract methods from the base class:
public abstract IDynamicDataServiceMetadataProvider GetMetadataProvider();


public abstract IDataServiceQueryProvider GetQueryProvider(IDataServiceMetadataProvider metadata);

The DomainModelService generates types using the Data Type Generator to generate the Entity types based on metadata that is provided to the service when it is instantiated. The metadata is also used to create the DynamicDataServiceMetadataProvider instance, the DomainModelService class will be discussed in more detail later in this post.

DynamicDataServiceMetadataProvider derives from the IDynamicDataServiceMetadataProvider interface, the IDynamicDataServiceMetadataProvider interface is derived from the IDataServiceMetadataProvider interface, its purpose is to simply facilitate unit testing. The DynamicDataServiceMetadataProvider class is simply a container for the metadata for the OData service.

DynamicDataServiceQueryProvider derives from the IDataServiceQueryProvider interface and provides the implementation of the IQueryable GetQueryRootForResourceSet(ResourceSet resourceSet)

DynamicDataServiceUpdateProvider derives from IDataServiceUpdateProvider interface and provides the mechanisms by which the OData service framework can create update and delete the exposed data type entities.

In summary the key classes of the implementation are the DomainModelService and the DynamicDataContext. The other classes in reality are pretty much boiler plate and depend mainly on the implementation of the DynamicDataContext.

DomainModelService

The DomainModelService is the primary touch point for exposing the dynamically defined entities types via the WCF OData service framework.
[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class DomainModelService : DynamicDataService
{
    private const String AssemblyName = "DomainModel";

    private Dictionary<EntityDescriptor, Type> _entityToTypeMap;
    private Dictionary<ResourceSet, Type> _resourceSetToTypeMap;
    private IDynamicTypeGenerator _dynamicTypeGenerator;
   
    /// <summary>
    /// Initializes a new instance of the <see cref="DomainModelService"/> class.
    /// </summary>
    /// <param name="domainDescriptor">The <see="DomainModelDescriptor"> that this service has to represent.</param>
    public DomainModelService(DomainModelDescriptor domainDescriptor) : this(domainDescriptor, DynamicTypeGenerator.Instance)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="DomainModelService"/> class.
    /// </summary>
    /// <param name="domainDescriptor">The <see="DomainModelDescriptor"> that this service has to represent.</param>
    /// <param name="dynamicTypeGenerator">The type genrator to use for generating the types</param>
    public DomainModelService(DomainModelDescriptor domainDescriptor, IDynamicTypeGenerator dynamicTypeGenerator)
    {
        _dynamicTypeGenerator = dynamicTypeGenerator;
        _entityToTypeMap = GenerateTypes(domainDescriptor);
    }

    /// <summary>
    /// Initializes the configuration for the service instance
    /// </summary>
    /// <param name="config">Configuration settings for the service.</param>
    public static void InitializeService(DataServiceConfiguration config)
    {
        config.SetEntitySetAccessRule("*", EntitySetRights.All);
        config.UseVerboseErrors = true;

        config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
        config.DataServiceBehavior.AcceptProjectionRequests = true;
    }

    /// <summary>
    /// Gets instance of an <see cref="IDynamicDataServiceMetadataProvider"/> derived type for this odata service.
    /// </summary>
    /// <returns>An instance of an <see cref="DynamicDataServiceMetadataProvider"/></returns>
    public override IDynamicDataServiceMetadataProvider GetMetadataProvider()
    {
        DynamicDataServiceMetadataProvider metaDataProvider = new DynamicDataServiceMetadataProvider(AssemblyName, "DomainModelService");
        _resourceSetToTypeMap = new Dictionary<ResourceSet, Type>();

        foreach (var entity in _entityToTypeMap.Keys)
        {
            Type t = _entityToTypeMap[entity];
            var resourceType = new ResourceType(t, ResourceTypeKind.EntityType, null, AssemblyName, entity.EntityName, false);

            foreach (var property in entity.EntityProperties)
            {
                ResourcePropertyKind kind = ResourcePropertyKind.Primitive;

                if (property.IsIdentity)
                {
                    kind |= ResourcePropertyKind.Key;
                }
               
                var resourceProperty = new ResourceProperty(property.Name,
                    kind, ResourceType.GetPrimitiveResourceType(property.Type));

                resourceType.AddProperty(resourceProperty);
            }

            metaDataProvider.AddResourceType(resourceType);
            ResourceSet resourceSet = new ResourceSet(String.Format("{0}s",entity.EntityName), resourceType);
            metaDataProvider.AddResourceSet(resourceSet);
            _resourceSetToTypeMap.Add(resourceSet, t);
        }

        return metaDataProvider;
    }

    /// <summary>
    /// Gets instance of an <see cref="IDataServiceMetadataProvider"/> derived type for this odata service.
    /// </summary>
    /// <param name="metadataProvider">The <see cref="IDataServiceMetadataProvider"/></param>
    /// <returns>An instance of an <see cref="DynamicDataServiceQueryProvider"/></returns>
    public override IDataServiceQueryProvider GetQueryProvider(IDataServiceMetadataProvider metadataProvider)
    {
        return new DynamicDataServiceQueryProvider(metadataProvider);
    }

    /// <summary>
    /// Create the underlying data source for this odata service
    /// </summary>
    /// <returns>An instance of an <see cref="DynamicDataContext"/> class</returns>
    protected override DynamicDataContext CreateDataSource()
    {
        return new DynamicDataContext(_resourceSetToTypeMap,
            ConfigurationManager.ConnectionStrings[DomainModelServiceSettings.Current.TargetStoreConnectionStringName].ConnectionString);
    }

    /// <summary>
    /// Generates types dynamically that represent the <see cref="EntityDescriptor"/> entities of the <paramref name="domainDescriptor"/>
    /// </summary>
    /// <param name="domainDescriptor">The <see cref="DomainModelDescriptor"/> that the types are to be generated for.</param>
    /// <returns>A <see cref="Dictionary"/> that contains mapping between an <see cref="EntityDescriptor"/> and the generated type</returns>
    private Dictionary<EntityDescriptor, Type> GenerateTypes(DomainModelDescriptor domainDescriptor)
    {
        Dictionary<EntityDescriptor, Type> map = new Dictionary<EntityDescriptor, Type>();
        AssemblyBuilder assemblyBuilder = null;
        ModuleBuilder moduleBuilder = null;
       
        _dynamicTypeGenerator.CreateDynamicAssembly(AssemblyName, out assemblyBuilder, out moduleBuilder);

        foreach (var entity in domainDescriptor.Entities)
        {
            map.Add(entity, _dynamicTypeGenerator.CreateLinqToSqlType(AssemblyName, assemblyBuilder, moduleBuilder, entity));
        }

        if (DomainModelServiceSettings.Current.StoreGeneratedAssemblies)
        {
            assemblyBuilder.Save(String.Format("{1}.dll", domainDescriptor.Version, AssemblyName));
        }

        return map;
    }
}

The service requires an instance of a DomainModelDescriptor class to be provided when the service is constructed. The DomainModelDescriptor defines the domain model entities and their properties that are to be exposed as an OData service. The constructor generates a set of types that map to the EntityDescriptor types that are contained within the DomainModelDescriptor instance.

metadata

When the OData framework invokes the on the object GetService(Type serviceType) method of the IServiceProvider interface this in turn invokes the two overridden methods GetMetadataProvider() and  GetQueryProvider(IDataServiceMetadataProvider metadata) methods.

Of the two methods only the GetMetadataProvider() is of interest. This method creates an instance of a DynamicDataServiceMetadataProvider type and populates it with the required ResourceSet, ResourceTypes, ResourceProperty values that will enable the OData framework to describe the underlying data source structure to an OData client.

Another key method is the CreateDataSource() method that creates the underlying data source for the OData service. In this implementation the data source is an instance of a DynamicDataContext. The DynamicDataServiceQueryProvider and DynamicDataServiceUpdateProvider types both leverage the functionality of the DynamicDataContext provider to perform their function.

DynamicDataContext

The DynamicDataContext class is a simple Linq To Sql DataContext derived class that is responsible for marshalling calls between the Custom Data Service Provider components and the underlying data store.
public class DynamicDataContext : DataContext, IDynamicDataContext
{
    private Dictionary<ResourceSet, Type> _resourceSetToTypeMap;


    /// <summary>
    /// Initializes a new instance of the <see cref="DynamicDataContext"/> class
    /// </summary>
    /// <param name="resourceSetToTypeMap">A <see cref="Dictionary"/> that contains a mapping from a <see cref="ResourceSet"/> to an underlying type</param>
    /// <param name="connection">The connection string for this <see cref="DataContext"/> derived type to use</param>
    public DynamicDataContext(Dictionary<ResourceSet, Type> resourceSetToTypeMap, String connection) : base(connection) 
    {
        _resourceSetToTypeMap = resourceSetToTypeMap;
    }


    /// <summary>
    /// Gets an instance of an <see cref="IQueryable"/> for the <paramref name="resourceSet"/>
    /// </summary>
    /// <param name="resourceSet">The resource set to retrieve the <see cref="IQueryable"/> instance for</param>
    /// <returns>
    /// An instance of an <see cref="IQueryable"/> type
    /// </returns>
    public IQueryable GetQueryableForResourceSet(ResourceSet resourceSet)
    {
        Type t = _resourceSetToTypeMap[resourceSet];


        return GetTableForType(t);
    }


    /// <summary>
    /// Add a resource to the underlying store
    /// </summary>
    /// <param name="resource">The resource to add to the store</param>
    public void AddResource(object resource)
    {
        ITable table = GetTableForType(resource.GetType());
        IEntity baseType = (IEntity) resource;


        if (baseType.Identifier == Guid.Empty)
        {
            baseType.Identifier = Guid.NewGuid();
        }


        table.Attach(resource);
    }


    /// <summary>
    /// Remove a resource from the underlying store
    /// </summary>
    /// <param name="resource">The resource to remove</param>
    public void RemoveResource(object resource)
    {
        ITable table = GetTableForType(resource.GetType());


        table.DeleteOnSubmit(resource);
    }


    /// <summary>
    /// Commit changes to the underlying storeB
    /// </summary>
    public void CommitChanges()
    {
        this.SubmitChanges();
    }


    private ITable GetTableForType(Type type)
    {
        MethodInfo method = typeof(DataContext).GetMethod("GetTable", Type.EmptyTypes);
        MethodInfo generic = method.MakeGenericMethod(type);
        return (ITable) generic.Invoke(this, null);
    }
}

As you can see there is nothing particularly complex about the DynamicDataContext. The GetQueryableForResourceSet simply maps ResourceSet to a type to provide an IQueryable object for querying data, which the OData framework will expose. When modifying or deleting entities in the data store the GetTableForType method uses some simple reflection to get the underlying ITable derived object to which changes are submitted.

Now we have a OData service that can be dynamically configured based on metadata. In the next post, Part 4 – WCF Service Host, I will cover the activation and instantiation of a DomainModelService.

No comments:

Post a Comment