Move from WCF Data Services to Web API – Part 2

From Part 1 of getting Metadata up and running we are now going to work on getting querying an entity set working.  But first a quick update.  From the last time I left getting the metadata in the processing request and I did move that out into it’s own method.

        protected HttpResponseMessage GenerateMetadataResponse()
        {
            var odataMediaTypeFormatter = new ODataMediaTypeFormatter(new DefaultODataDeserializerProvider(),
                                                                          new DefaultODataSerializerProvider(),
                                                                          new[] { ODataPayloadKind.MetadataDocument });

            //ToDo will need to make sure they can accept application/xml
            var formatter = odataMediaTypeFormatter.GetPerRequestFormatterInstance(typeof(IEdmModel), Request,
                                                                                   new MediaTypeHeaderValue(
                                                                                       "application/xml"));
            var response = Request.CreateResponse();
            response.Content = new ObjectContent(typeof(IEdmModel), GetMetadata(), formatter);
        }

Now the first thing we need to do is see if we are dealing with an entity set.

            var collectionType = path.EdmType as IEdmCollectionType;
            if (collectionType != null)
            {
                return QueryEntitySet(collectionType);
            }

If collectionType isn’t null then we will need to check for and retrieve the EntityType.

        private IEdmEntityType GetEdmEntityType(IEdmType edmType)
        {
            var edmEntityType = edmType as IEdmEntityType;
            if (edmEntityType == null)
            {
                var collectionType = edmType as IEdmCollectionType;
                if (collectionType != null)
                {
                    edmEntityType = collectionType.ElementType.AsEntity().EntityDefinition();
                }
            }

            return edmEntityType;
        }

If this method returns null then we are not dealing with an entity or entity set.  From there we will need to switch from IEdmEntityType to the CLR type.   For that I’m going to create another interface and create a default implementation for DbContext.

namespace ODataServices.Interfaces
{
    public interface IEdmEntityToClrConverter
    {
        Type AsClrType<TSource>(TSource source, IEdmEntityType edmEntityType);
    }
}

and here is the default implementation.  I’m not sold on how I’m looking up the CLR type from metadata of the DbContext and if someone else has a better idea let me know.  Or even better answer this stackoverflow question.

namespace ODataServices
{
    public class DbContextEdmEntityToClrConverter : IEdmEntityToClrConverter
    {
        private readonly static ConcurrentDictionary<IEdmEntityType, Type> _cachedConversions =
            new ConcurrentDictionary<IEdmEntityType, Type>();

        public Type AsClrType<TSource>(TSource source, IEdmEntityType edmEntityType)
        {
            var dbContext = source as DbContext;
            if (dbContext == null)
            {
                return null;
            }

            // Can't use GetOrAdd want to trap for null
            Type result;
            if (!_cachedConversions.TryGetValue(edmEntityType, out result))
            {
                result = ConvertIEdmEntityTypeToClr(edmEntityType, dbContext);
                if (result != null)
                {
                    _cachedConversions.TryAdd(edmEntityType, result);
                }
            }

            return result;
        }

        private Type ConvertIEdmEntityTypeToClr(IEdmEntityType edmEntityType, DbContext context)
        {
            var metadata = ((IObjectContextAdapter)context).ObjectContext.MetadataWorkspace;
            var oSpace = metadata.GetItemCollection(DataSpace.OSpace);
            var typeName = oSpace.GetItems<EntityType>().Select(e => e.FullName).FirstOrDefault(name =>
            {
                var fullname = name + ":" + edmEntityType.FullName();
                MappingBase map;
                return metadata.TryGetItem(fullname, DataSpace.OCSpace, out map);
            });

            if (typeName != null)
            {
                return Type.GetType(typeName, null, GetTypeFromAssembly, false, false);
            }
            return null;
        }

        private Type GetTypeFromAssembly(Assembly assembly, string nameOfType, bool ignoreCase)
        {
            if (assembly == null)
            {
                var resolver = new DefaultAssembliesResolver();
                return resolver.GetAssemblies()
                               .Select(a => a.GetType(nameOfType, false, ignoreCase))
                               .FirstOrDefault(t => t != null);
            }
            return assembly.GetType(nameOfType, false, ignoreCase);
        }
    }
}

Noticed I had to  use an Assembly Resolver otherwise Type.GetType() would fail.  Now that we have a type I’m going to use a bit of reflection and switch us from a System Type to a generic type.  I’m going to store a method handle to the method we want to switch to and grab that in the static constructor since it shouldn’t change. Also I’m going to add a couple more interfaces we haven’t talk about yet but will quickly discuss.

        static ODataServicesController()
        {
            _processRequest =
                typeof (ODataServicesController<TSource>).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
                                                         .First(
                                                             m =>
                                                             m.Name == "ProcessRequest" && m.IsGenericMethodDefinition)
                                                         .MethodHandle;

            // Load up default implementations
            _serviceLocatorDefaults[typeof (IEdmModelFactory)] =
                new Lazy<object>(() => new DbContextEdmModel());
            _serviceLocatorDefaults[typeof (IEdmEntityToClrConverter)] =
                new Lazy<object>(() => new DbContextEdmEntityToClrConverter());
            _serviceLocatorDefaults[typeof (IQueryRootProvider)] =
                new Lazy<object>(() => new DbContextQueryRoot());
        }

        private static readonly RuntimeMethodHandle _processRequest;

IQueryRootProvider will be used to return the starting point of the IQueryable.  IQueryInterceptor is used to add a where clause.  I was on the fence on that interface as it could be implemented in IQueryRootProvider but added it for flexibility and since WCF Data Services has something similar.

namespace ODataServices.Interfaces
{
    public interface IQueryRootProvider
    {
        IQueryable<TEntity> QueryRoot<TSource, TEntity>(TSource source)
            where TEntity : class;
    }
}

namespace ODataServices.Interfaces
{
    public interface IQueryInterceptor
    {
        Expression<Func<TEntity, bool>> Intercept<TEntity>();
    }
}

Here is the default implementation of IQueryRootProvider for DbContext

namespace ODataServices
{
    public class DbContextQueryRoot : IQueryRootProvider
    {
        public IQueryable<TEntity> QueryRoot<TSource, TEntity>(TSource source)
            where TEntity : class
        {
            var dbContext = source as DbContext;
            if (dbContext == null)
            {
                return null;
            }

            return QueryRoot<TEntity>(dbContext);
        }

        private IQueryable<TEntity> QueryRoot<TEntity>(DbContext dbContext)
            where TEntity : class
        {
            return dbContext.Set<TEntity>().AsNoTracking();
        }
    }

Pretty simple stuff. For IQueryInterceptor I created a default implementation but it doesn’t rely on DbContext. First I created an attribute called QueryInterceptorAttribute, same name as WCF Data Services.

namespace ODataServices.Attributes
{
    [AttributeUsageAttribute(AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
    public class QueryInterceptorAttribute : Attribute
    {
    }
}

namespace ODataServices
{
    public abstract class QueryInterceptor : IQueryInterceptor
    {
        private readonly MethodInfo[] _methodInfos;

        protected QueryInterceptor()
        {
            _methodInfos = GetType().GetMethods()
                                    .Where(
                                        m =>
                                        m.GetCustomAttributesData()
                                         .Any(a => a.AttributeType == typeof(QueryInterceptorAttribute)) &&
                                        !m.GetParameters().Any() &&
                                        m.IsGenericMethod == false).ToArray();
        }

        public Expression<Func<TEntity, bool>> Intercept<TEntity>()
        {
            var inteceptor = _methodInfos.FirstOrDefault(m => m.ReturnType == typeof(Expression<Func<TEntity, bool>>));
            if (inteceptor != null)
            {
                return inteceptor.Invoke(this, new object[0]) as Expression<Func<TEntity, bool>>;
            }
            return null;
        }
    }
}

With the abstract class of QueryInterceptor you can create a class that inherits from it and mark methods with the QueryInterceptor attribute. If the return type of the method is type we are looking for it will auto add the return value to the where clause.  If you don’t like this implementation you can just create a different class that implements IQueryInterceptor, that’s the great part.  Also word of warning I haven’t tested that code yet 🙂

Now to put it all together

        protected HttpResponseMessage QueryEntitySet(IEdmCollectionType edmCollectionType)
        {
            var edmType = GetEdmEntityType(edmCollectionType);
            if (edmType != null)
            {
                var entityType = GetIEdmTypeToCLRType(edmType);
                // Switch from system Type to generic type
                var process = (Func<IEdmEntityType, IQueryable>)
                              _typeToGeneric.GetOrAdd(new KeyValuePair<IEdmEntityType, Type>(edmType, entityType),
                                                      pair =>
                                                      {
                                                          var method = ((MethodInfo)
                                                                        MethodBase.GetMethodFromHandle(
                                                                            _processRequest, GetType().TypeHandle))
                                                              .MakeGenericMethod(pair.Value);
                                                          return
                                                              Delegate.CreateDelegate(
                                                                  typeof(Func<IEdmEntityType, IQueryable>), this,
                                                                  method);
                                                      });
                var query = process(edmType);
                var response = Request.CreateResponse();
                var odataMediaTypeFormatter = new ODataMediaTypeFormatter(new DefaultODataDeserializerProvider(),
                                                                          new DefaultODataSerializerProvider(),
                                                                          new[] { ODataPayloadKind.Feed });

                //ToDo will need to use a content negotiator - hard code to XML for now
                var formatter = odataMediaTypeFormatter.GetPerRequestFormatterInstance(typeof(IEdmCollectionType), Request,
                                                                                       new MediaTypeHeaderValue(
                                                                                           "application/xml"));
                response.Content = new ObjectContent(typeof(IEnumerable<>).MakeGenericType(query.ElementType), query, formatter);
                return response;
            }
            throw new Exception("Entity Set not found!");
        }

        private IQueryable ProcessRequest<TEntity>(IEdmEntityType edmEntityType)
            where TEntity : class
        {

            var queryable = GetQueryRoot<TEntity>();

            // Check if there are any interceptor
            var interception = GetInterceptor<TEntity>();
            if (interception != null)
            {
                queryable = queryable.Where(interception);
            }

            var model = GetMetadata();

            // need to tell Web API about the mapping between IEdmEntityType and the CLR Type
            model.SetAnnotationValue(edmEntityType, new ClrTypeAnnotation(typeof (TEntity)));
            var queryContext = new ODataQueryContext(model, typeof (TEntity));
            var queryOptions = new ODataQueryOptions(queryContext, Request);

            //ToDo this does the default Web API filtering but would like to get more control over it
            return queryOptions.ApplyTo(queryable);
        }

        private IQueryable<TEntity> GetQueryRoot<TEntity>()
            where TEntity : class
        {
            var queryRoot = ServiceLocator<IQueryRootProvider>();
            if (queryRoot != null)
            {
                return queryRoot.QueryRoot<TSource, TEntity>(CurrentDataSource);
            }
            return null;
        }

        private Expression<Func<TEntity, bool>> GetInterceptor<TEntity>()
        {
            var interception = ServiceLocator<IQueryInterceptor>();
            if (interception != null)
            {
                return interception.Intercept<TEntity>();
            }
            return null;
        }

This is what the ProcessRequest method looks like now and I added to dispose of the Data Source.

        protected virtual HttpResponseMessage ProcessRequest()
        {
            var pathHandler = Request.GetODataPathHandler();
            var metadata = GetMetadata();
            var path = pathHandler.Parse(metadata, GetODataPath());
            Request.SetODataPath(path);
            Request.SetODataRouteName("OData");
            if (path.Segments.Any(s => s.SegmentKind == ODataSegmentKinds.Metadata))
            {
                return GenerateMetadataResponse();
            }

            var collectionType = path.EdmType as IEdmCollectionType;
            if (collectionType != null)
            {
                return QueryEntitySet(collectionType);
            }

            return new HttpResponseMessage(HttpStatusCode.NotImplemented);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_currentDataSource.IsValueCreated)
                {
                    var disposer = CurrentDataSource as IDisposable;
                    if (disposer != null)
                    {
                        disposer.Dispose();
                    }
                }
            }
            base.Dispose(disposing);
        }

Now we should be able to hit /api/odata/Customers?$orderby=City&$select=City or /api/odata/Customers. Assuming your DbContext has an Customer Entity Set and property called city 🙂 Next I will be trying to get a single entity but that’s for a Part 3 and we’ll need to dive into ExpressionTrees.

Tags: , ,

Tuesday, April 15th, 2014 OData

Leave a Reply

*