共用方式為


Actions in WCF Data Services – Part 3: A sample provider for the Entity Framework

This post is the last in a series on Actions in WCF Data Services. The series was started with an example experience for defining actions (Part 1) and how IDataServiceActionProvider works (Part 2). In this post we’ll go ahead and walk through a sample action provider implementation that delivers the experience outlined in part 1 for the Entity Framework.

If you want to walk through the code while reading this you’ll find the code here: https://efactionprovider.codeplex.com.

Implementation Strategy

The center of the implementation is the EntityFrameworkActionProvider class that derives from ActionProvider. The EntityFrameworkActionProvider is specific to EF, whereas the ActionProvider provides a generic starting point for an implementation of IDataServiceActionProvider that enables the experience outlined in Part 1.

There is quite a bit going on in the sample code, so I can’t walk through all of it in a single blog post, instead I’ll focus on the most interesting bits of the code:

Finding Actions

Data Service providers can be completely dynamic, producing a completely different model for each request. However with the built-in Entity Framework provider the model is static, basically because the EF model is static, also the actions are static too, because they are defined in code using methods with attributes on them. This all means one thing – our implementation can do a single reflection pass to establish what actions are in the model and cache that for all requests.

So every time the EntityFrameActionProvider is constructed, it first checks a cache of actions defined on our data source, which in our case is a class derived from EF’s DBContext. If a cache look up it successful great, if not it uses the ActionFactory class to go an construct ServiceActions for every action declared on the DBContext.

The algorithm for the ActionFactory is relatively simple:

  • It is given the Type of the class that defines all the Actions. For us that is the T passed to DataService<T> , which is a class derived from EF’s DBContext.
  • It them looks for methods with one of these attributes [Action] , [NonBindableAction] or [OccasionallyBindableAction] , all of which represent different types of actions.
  • For each method it finds it then uses the IDataServiceMetadataProvider it was with constructed with to convert the parameters and return types into ResourceTypes.
  • At this point it can construct a ServiceAction for each.

Parameter Marshaling

When an action is actually invoked we need to convert any parameters from WCF Data Services internal representation into objects that the end users methods can actually handle. It is likely that marshaling will be quite different for different underlying providers (EF, Reflection, Custom etc), so the ActionProvider uses an interface IParameterMarshaller, whenever it needs to convert parameters. The EF’s parameter marshaled looks like this:

public class EntityFrameworkParameterMarshaller: IParameterMarshaller
{
    static MethodInfo CastMethodGeneric = typeof(Enumerable).GetMethod("Cast");
    static MethodInfo ToListMethodGeneric = typeof(Enumerable).GetMethod("ToList");

    public object[] Marshall(DataServiceOperationContext operationContext, ServiceAction action, object[] parameters)
    {
        var pvalues = action.Parameters.Zip(parameters, (parameter, parameterValue) => new { Parameter = parameter, Value = parameterValue });
        var marshalled = pvalues.Select(pvalue => GetMarshalledParameter(operationContext, pvalue.Parameter, pvalue.Value)).ToArray();

        return marshalled;
    }
    private object GetMarshalledParameter(DataServiceOperationContext operationContext, ServiceActionParameter serviceActionParameter, object value)
    {
        var parameterKind = serviceActionParameter.ParameterType.ResourceTypeKind;
           
        // Need to Marshall MultiValues i.e. Collection(Primitive) & Collection(ComplexType)
        if (parameterKind == ResourceTypeKind.EntityType)
        {

            // This entity is UNATTACHED. But people are likely to want to edit this...
            IDataServiceUpdateProvider2 updateProvider = operationContext.GetService(typeof(IDataServiceUpdateProvider2)) as IDataServiceUpdateProvider2;
            value = updateProvider.GetResource(value as IQueryable, serviceActionParameter.ParameterType.InstanceType.FullName);
            value = updateProvider.ResolveResource(value); // This will attach the entity.
        }
        else if (parameterKind == ResourceTypeKind.EntityCollection)
        {
            // WCF Data Services constructs an IQueryable that is NoTracking...
            // but that means people can't just edit the entities they pull from the Query.
            var query = value as ObjectQuery;
            query.MergeOption = MergeOption.AppendOnly;
        }
        else if (parameterKind == ResourceTypeKind.Collection)
        {
            // need to coerce into a List<> for dispatch
            var enumerable = value as IEnumerable;
            // the <T> in List<T> is the Instance type of the ItemType
            var elementType = (serviceActionParameter.ParameterType as CollectionResourceType).ItemType.InstanceType;
            // call IEnumerable.Cast<T>();
            var castMethod = CastMethodGeneric.MakeGenericMethod(elementType);
            object marshalledValue = castMethod.Invoke(null, new[] { enumerable });
            // call IEnumerable<T>.ToList();
            var toListMethod = ToListMethodGeneric.MakeGenericMethod(elementType);
            value = toListMethod.Invoke(null, new[] { marshalledValue });
        }
        return value;
    }
}

This is probably the hardest part of the whole sample because it involves understanding what is necessary to make the parameters you pass to the service authors action methods updatable (remembers Actions generally have side-effects).

For example when Data Services see’s something like this: POST https://server/service/Movies(1)/Checkout

It builds a query to represent the Movie parameter to the Checkout action, however when Data Services is building queries, it doesn’t need the Entity Framework to track the results – because all it is doing is serializing the entities and then discarding them. However in this example, we need to take the query and actually retrieve the object in such as way that it is tracked by EF, so that if it gets modified inside the action EF will notice and push changes back to the Database during SaveChanges.

Delaying invocation

As discussed in Part 2, we need to delay actual invocation of the action until SaveChanges(), to do this we return an implementation of an interface called IDataServiceInvokable:

public class ActionInvokable : IDataServiceInvokable
{
    ServiceAction _serviceAction;   
    Action _action;
    bool _hasRun = false;
    object _result;

    public ActionInvokable(DataServiceOperationContext operationContext, ServiceAction serviceAction, object site, object[] parameters, IParameterMarshaller marshaller)
    {
        _serviceAction = serviceAction;
        ActionInfo info = serviceAction.CustomState as ActionInfo;
        var marshalled = marshaller.Marshall(operationContext,serviceAction,parameters);

        info.AssertAvailable(site,marshalled[0], true);
        _action = () => CaptureResult(info.ActionMethod.Invoke(site, marshalled));
    }
    public void CaptureResult(object o)
    {
        if (_hasRun) throw new Exception("Invoke not available. This invokable has already been Invoked.");
        _hasRun = true;
        _result = o;
    }
    public object GetResult()
    {
        if (!_hasRun) throw new Exception("Results not available. This invokable hasn't been Invoked.");
        return _result;
    }
    public void Invoke()
    {
        try
        {
            _action();
        }
        catch {
            throw new DataServiceException(
                500,
                string.Format("Exception executing action {0}", _serviceAction.Name)
            );
        }
    }
}

As you can see this does a couple of things, it creates an Action (this time a CLR one - just to confuse the situation i.e. a delegate that returns void), that actually calls the method on your DBContext via reflection passing the marshaled parameters. It also has a few guards, one to insure that Invoke() is only called once, and another to make sure GetResult() is only called after Invoke().

Actions that are bound *sometimes*

Part 1 and 2 introduce the concept of occasionally bindable actions, but basically an action might not always be available in all states, for example you might not always be able to checkout a movie (perhaps you already have it checked out).

The ActionInfo class has an IsAvailable(…) method which is used by the ActionProvider whenever WCF Data Services need to know if an Action should be advertised or not. The implementation of this will call the method specified in the [OccasionallyBindableAction] attribute. The code is complicated because it supports always return true without actually checking if the actual availability method is too expensive to call repeatedly. This is indicated using the [SkipCheckForFeeds] attribute.

Getting the Sample Code

The sample is on codeplex made up of two projects:

  • ActionProviderImplementation – the actual implementation of the Entity Framework action provider.
  • MoviesWebsite – a sample MVC3 website that exposes a simple OData Service

Summary

For most of you downloading the samples and using them to create a OData Service with Actions over the Entity Framework is all you’ll need.

However if you are actually considering tweaking the example or building your own from scratch hopefully you now have enough context!

Happy coding
Alex James
Senior Program Manager, Microsoft.

Comments

  • Anonymous
    April 12, 2012
    Alex, I downloaded the code and built the project. How do I run it? What does it do?

  • Anonymous
    April 12, 2012
    Run the code, browse to service, hit the movies feed, and you'll notice actions advertised in the atom invoke them as described in Part 1. If you have a breakpoint in your action method on the MoviesContext it will stop in the method, and you can see how it works end to end, and perhaps you can then try adding your own actions. Good luck

  • Anonymous
    April 20, 2012
    The comment has been removed

  • Anonymous
    May 11, 2012
    anyone figured out the above error?

  • Anonymous
    May 16, 2012
    Hi Alex, I have a design question about using WCF Data Services and ServiceActions.  We are writing an Azure application that sits on top of a database.  It contains an aggregate of data from several other servers running at client sites.  Our data structures are a common subset of several different data owners, so we don't know the full structure of their data.  If we want to pass  data back and forth, should we write an OData Service on our end that everyone can use and have them all implement the same API that implements some IDataServiceActionProvider? Any advice is appreciated.  We are building this from the ground up.

  • Anonymous
    January 07, 2013
    Can anyone please provide sample code how to invoke the service action method public void RateAll(IQueryable<Movie> movies, IEnumerable<int> ratings) Thanks a lot