Udostępnij za pośrednictwem


Actions in WCF Data Services – Part 2: How IDataServiceActionProvider works

In this post we will explorer the IDataServiceActionProvider interface, which must be implemented to add Actions to a WCF Data Service.

However if you are simply creating an OData Service and you can find an implementation of IDataServiceActionProvider that works for you (I’ll post sample code with Part 3) then you can probably skip this post.

Now before we continue, to understand this post fully you’ll need to be familiar with Custom Data Service Providers and a good place to start is here.

IDataServiceActionProvider

Okay so lets take a look at the actions interface:

public interface IDataServiceActionProvider
{
bool AdvertiseServiceAction(
DataServiceOperationContext operationContext,
ServiceAction serviceAction,
object resourceInstance,
bool resourceInstanceInFeed,
ref ODataAction actionToSerialize);

IDataServiceInvokable CreateInvokable(
DataServiceOperationContext operationContext,
ServiceAction serviceAction,
object[] parameterTokens);

IEnumerable<ServiceAction> GetServiceActions(
DataServiceOperationContext operationContext);

IEnumerable<ServiceAction> GetServiceActionsByBindingParameterType(
DataServiceOperationContext operationContext,
ResourceType bindingParameterType);

bool TryResolveServiceAction(
DataServiceOperationContext operationContext,
string serviceActionName,
out ServiceAction serviceAction);
}

You’ll notice that every method either takes or returns a ServiceAction. ServiceAction is the metadata representation of an Action, that includes information like the Action name, its parameters, its ReturnType etc.

When you implement IDataServiceActionProvider you are augmenting the metadata for your service which is defined by your services implementation of IDataServiceMetadataProvider with Actions and handling dispatch to those actions as appropriate.

We added this new interface rather than creating a new version of IDataServiceMetadataProvider because we didn’t have time to add an Action implementation for the built-in Entity Framework and Reflection provider, but we still wanted you be able to add actions when using those providers. This separation of concerns allows you to use the built-in providers and layer in support for Actions on the side.

However one problem remains: to create a new Action you will need access to the ResourceTypes in the your service, so you can create Action parameters and specify Action ReturnTypes. Previously you couldn’t get at the ResourceTypes unless you created a completely custom provider. So to give you access to the ResourceTypes we added an implementation of IServiceProvider to the DataServiceOperationContext class which is passed to every one of the above methods.

Now anywhere you have one of these operationContexts you can get the current implementation of IDataServiceMetadataProvider (and thus the ResourceTypes) like this:

var metadata = operationContext.GetService(typeof(IDataServiceMetadataProvider)) as IDataServiceMetadataProvider;

Exposing ServiceActions

There are 3 methods that the Data Services Server uses to learn about actions:

  • GetServiceActions(DataServiceOperationContext) – returns every ServiceAction in the service, and is used when we need all the metadata, i.e. if someone goes to $metadata
  • GetServiceActionsByBindingParameterType(DataServiceOperationContext,ResourceType) – returns every ServiceAction that can be bound to the ResourceType specified. This is used when we are returning an entity and we want to include information about Actions that can be invoked against that entity. The contract here is you should only include Actions that take the specified ResourceType exactly (i.e. no derived types) as the binding parameter to the action. We will call this method once for each ResourceType we encounter during serialization.
  • TryResolveServiceAction(DataServiceOperationContext,serviceActionName,out serviceAction) – return true if a ServiceAction with the specified name is found.

Now you could clearly implement both GetServiceActionsByBindingParameterType(..) and TryResolveServiceAction(..) by calling GetServiceActions(..), but Data Services tries to avoid loading all the metadata at once wherever possible, so you get the opportunity to provide more efficient targeted implementations.

Basically 99% of the time Data Services doesn’t need every ServiceAction, so it won’t ask for all of them most of the time.

To expose an Action you simply create a ServiceAction and return it from these methods as appropriate. For example to create a ServiceAction that corresponds to this C# signature (where Movie is an entity):

void Rate(Movie movie, int rating)

You would do something like this:

ServiceAction movieRateAction = new ServiceAction(
“Rate”, // name of the action
null, // no return type i.e. void
null, // no return type means we don’t need to know the ResourceSet so use null.
OperationParameterBindingKind.Always, // i.e this action is always bound to an Movie entities
// other options are Never and Always.
new [] {
new ServiceActionParameter(“movie”, movieResourceType),
new ServiceActionParameter(“rating”, ResourceType.GetPrimitiveType(typeof(int)))
}
);

As you can see nothing too tricky here.

Advertizing ServiceActions

If you looked at the first post you’ll remember that some Actions are available only in certain states. This is configured when you create your the ServiceAction, something like this:

ServiceAction checkoutMovieAction = new ServiceAction(
“Checkout”, // name of the action
ResourceType.GetPrimitiveType(typeof(bool)), // Edm.Boolean is the returnType
null, // the returnType is a bool, so it doesn’t have a ResourceSet
OperationParameterBindingKind.Sometimes, // You can’t always checkout a movie
new [] { new ServiceActionParameter(“Movie”, movieResourceType) }
);

Notice in this example the OperationParameterBindingKind is set to Sometimes which means the Checkout Action is not available for every Movie. So when DataServices returns a Movie it will check with the ActionProvider to see if the Action is currently available. Which it does by calling:

bool AdvertiseServiceAction(
DataServiceOperationContext operationContext,
ServiceAction serviceAction, // the action that the server knows MAY be bound.
object resourceInstance, // the entity which MAY allow the action to be bound.
bool resourceInstanceInFeed, // whether the server is serializing a single entity or a feed (expect multiple calls).
ref ODataAction actionToSerialize); // modifying this parameter allows you to do customize things like the URL
// the client will POST to to invoke the action.

For example you might check if the current user (i.e. HttpContext.Current.User) has a Movie checked out already, to decide whether they can Checkout that Movie or not.

The resourceInstanceInFeed parameter needs a special mention. Sometimes working out whether an Action is available is time or resource intensive, for example if you have to do a separate database query. Generally this isn’t a problem if you are returning just one Entity, but if you are returning a whole feed of entities it is clearly undesirable. The OData protocol says that in situations like this you should err by exposing actions that aren’t actually available (and fail later if they are invoked). WCF Data Services doesn’t know if it is expensive to establish action availability, so to help you decide whether to do the check it lets you know whether you are in a feed or not. This way your Action provider can just return true, it if knows it is costly to calculate and it is in a feed.

Invoking ServiceActions

When an action is invoked, your implementation of this is called:

IDataServiceInvokable CreateInvokable(
DataServiceOperationContext operationContext,
ServiceAction serviceAction,
object[] parameterTokens);

Notice you don’t actually invoke the action immediately instead you return an implementation of IDataServiceInvokable, which looks like this:

public interface IDataServiceInvokable
{
object GetResult();
void Invoke();
}

As you can see this is a simple interface, but why do we delay calling the action?

Well actions generally have side-effects so they need to work in conjunction with the UpdateProvider (or IDataServiceUpdateProvider2), to actually save those changes to disk. To support Actions you need an Update Provider like the built-in Entity Framework provider that implements the new IDataServiceUpdateProvider2 interface:

public interface IDataServiceUpdateProvider2 : IDataServiceUpdateProvider, IUpdatable
{
void ScheduleInvokable(IDataServiceInvokable invokable);
}

This allows WCF Data Services to schedule arbitrary work to happen during IDataServiceUpdateProvider.SaveChanges(..), which allows update providers and action providers to be written independently. Which is great because if you are using the Entity Framework you really don’t want to have to write an update provider from scratch.

Now when you implement IDataServiceInvokable you are responsible for 3 things:

  1. Capturing and potentially marshaling the parameters.
  2. Dispatching the parameters to the code that actually implements the Action when Invoke() is called.
  3. Storing any results from Invoke() so they can be retrieved using GetResult()

The parameters themselves are passed as tokens. This is because it is possible to write a Data Service Provider that works with tokens that represent resources, if this is the case you may need to convert these token into actual resources before dispatching to the actual action. What is required depends 100% on the rest of the provider code, so it is impossible to say exactly what you need to do here. However in part 3 well explore doing this for the Entity Framework.

If the first parameter to the action is a binding parameter (i.e. an EntityType or a Collection of EntityTypes) it will be passed as an un-enumerated IQueryable. Most of the time this isn’t too interesting but it does mean you can do nifty tricks like write an action that doesn’t actually retrieve the entities from the database if appropriate.

Summary

This post walked you through the design of IDataServiceActionProvider and the expectations for people implementing this interface. While this is quite a tricky interface to implement, it is low level code and hopefully you will be able to find an existing implementation that works for you. Indeed in Part 3 we will share and walk through an sample implementation for the Entity Framework designed to deliver the Service Author Experience we introduced in Part 1.

If you have any questions let me know.

Alex James
Senior Program Manager, Microsoft

Comments

  • Anonymous
    July 29, 2013
    Hi Alex, I'm trying to implement this approach, however..... Any json request to the service is now failing and i beleive it is the action metadata causing the issue.  See example below where I believe the $metadata# is causing the json not to parse. <m:action metadata="127.0.0.1/.../mySvc.svc$metadata#myEntities.Like" title="Like" target="127.0.0.1/.../Like" /><m:action Below is the error when I publish the Actions onto the feed.... Microsoft.Data.OData.ODataException: A node of type 'EndOfInput' was read from the JSON reader when trying to read the start of an entry. A 'StartObject' node was expected.   at Microsoft.Data.OData.Json.ODataJsonEntryAndFeedDeserializer.ReadEntryStart()   at Microsoft.Data.OData.Json.ODataJsonReader.ReadEntryStart()   at Microsoft.Data.OData.Json.ODataJsonReader.ReadAtStartImplementation()   at Microsoft.Data.OData.ODataReaderCore.ReadImplementation()   at Microsoft.Data.OData.ODataReaderCore.ReadSynchronously()   at Microsoft.Data.OData.ODataReaderCore.InterceptException[T](Func`1 action)   at Microsoft.Data.OData.ODataReaderCore.Read()   at System.Data.Services.Serializers.EntityDeserializer.ReadEntry(ODataReader odataReader, SegmentInfo topLevelSegmentInfo)   at System.Data.Services.Serializers.EntityDeserializer.Read(SegmentInfo segmentInfo)   at System.Data.Services.Serializers.ODataMessageReaderDeserializer.Deserialize(SegmentInfo segmentInfo) Your guidance would be appreciated!

  • Anonymous
    July 29, 2013
    Forget it! I was using Fiddler and forgot to set my request to Get, was set to post!