Actions in WCF Data Services


“Actions will provide a way to inject behaviors into an otherwise data-centric model without confusing the data aspects of the model, while still staying true to the resource oriented underpinnings of OData."

The October 2011 CTP of WCF Data Services adds powerful, but incomplete support for Actions. The motivation behind Actions stems from wanting to advertise in an OData entry an invocable ‘Action’ that has a side-effect on the OData service.

This statement is broad, but deliberately so; Actions have a lot of power.

Using WCF Data Services to Invoke an Action:

This release’s WCF Data Services client can invoke Actions that have no parameters with any return type (i.e. void, Feed, Entry, ComplexType, Collection of ComplexType, PrimitiveType or Collection of PrimitiveType.

To invoke Actions you call either Execute(..) for void actions or Execute<T>(..) for everything else. For example:

var checkedOut = ctx.Execute<bool>(
   new Uri(“
http://server/service.svc/Movies(6)/Checkout”),
   HttpMethod.Post,
   true
).Single();

Here the Execute<T> function takes the Uri of the Action you want to invoke, the HttpMethod to use (which in this case is Post because we are invoking a side-effecting action), and singleResult=true to indicate there is only one result (i.e. it is not a collection). The method returns a QueryOperationResponse<bool>, which implements IEnumerable<bool>, so we call Single() to get the lone boolean that is the result of invoking the action.

NOTE: Needing to specify singleResult=true is a temporary CTP only requirement, because in the CTP our deserialization code can’t automatically detect whether the result is a collection or single result.

A nice side-effect of this new feature is that you can now call ServiceOperations too, so long as you craft the full Uri (including any parameters) yourself. For example, the code below calls a ServiceOperation called GetMoviesByGenre that takes a single parameter called Genre and returns a Collection (or feed) of Movies using a Get:

var movies = ctx.Execute<Movie>(
   new Uri(“
http://server/service.svc/GetMoviesByGenre?genre=’Comedy’”),
   HttpMethod.Get,
   true
);

foreach(var movie in movies) {
   // do something
}

Coming Soon…

By RTM we plan to add full support for parameters, both for actions and service operations.

The current plan is for a new BodyParameter class that could be used to specify Actions parameters like this:

var checkedOutForAWeek = ctx.Execute<bool>(
   new Uri(“
http://server/service.svc/Movies(6)/Checkout”),
   HttpMethod.Post,
   new BodyParameter("noOfDays", 7)
).Single();

And a new UriParameter class that could be used to specify ServiceOperation parameters too:

var movies = ctx.Execute<Movie>(
   new Uri("
http://server/service.svc/GetMoviesByGenre"),
   HttpMethod.Get,
   new UriParameter("genre", "Comedy")
);

Setting up a WCF Data Service with Actions:

Unfortunately, creating actions with WCF Data Services in this release is quite tricky because it requires a completely Custom Data Service Provider, but we are striving to make this easy by RTM.

This CTP’s WCF Data Services Server only supports one parameter (i.e. the binding parameter), again this will change by RTM.

To get started with actions, first create a ServiceAction in your IDataServiceMetadataProvider2 implementation; something like this:

ServiceAction checkout = new ServiceAction(
   "Checkout",
   ResourceType.GetPrimitiveResourceType(typeof(bool)),
   null,
   new List<ServiceOperationParameter>{
      new ServiceOperationParameter("movie", movieResourceType)
   },
   true
);
checkout.SetReadOnly();

ServiceAction currently derives from ServiceOperation, so you will need to add any ServiceActions that you create to the collection of ServiceOperations you expose via both IDataServiceMetadataProvider.ServiceOperations and IDataServiceMetadataProvider.TryResolveServiceOperation(..). Also because Data Services are locked down by default you will need to configure your service to expose your actions using SetServiceOperationAccessRule(..).

IDataServiceMetadataProvider2 also adds a new method to find actions possibly bound to a ResourceType instance, (i.e. to an individual Movie). This is so that when the WCF Data Service is serializing Entities it doesn’t need to walk over all the metadata to find Actions that might bind to a particular entity. Here is a naïve implementation, where _sop is a list of all ServiceOperations:

public IEnumerable<ServiceOperation> GetServiceOperationsByResourceType(ResourceType resourceType)
{
   return _sops.OfType<ServiceAction>()
               .Where(a => a.Parameters.Count > 0 && a.Parameters.First().ParameterType == resourceType);
}

Next implement IDataServiceQueryProvider2.IsServiceOperationAdvertisable(..) to tell Data Services whether an Action should be advertised on a particular entity:

public bool IsServiceOperationAdvertisable(
   object resourceInstance,
   ServiceOperation serviceOperation,
   ref Microsoft.Data.OData.ODataOperation operationToSerialize
){
   Movie m = resourceInstance as Movie;
   if (m == null) return false;
   var checkedOut = GetIsCheckedOut(m, HttpContext.Current.User);

   if (serviceOperation.Name == "Checkout" && !checkedOut) return true;
   else if (serviceOperation.Name == "Checkin" && checkedOut) return true;
   else return false;
}

Here resourceInstance is the instance that is being serialized to the client, serviceOperation is the ServiceAction that the server is considering advertising, and operationToSerialize is an OData-structure representing the action information that’ll be serialized if you return true (note you can change properties on this class if for example you want to override the title or target of the Action in the payload).

As you can see, this code knows that only Movies have actions, and that it has only two actions; Checkin and Checkout. It calls an implementation-specific method to work out whether the current user has the current movie checked out and then uses this information to decide whether to advertise the Action.

Next you need to implement IDataServiceUpdateProvider2.InvokeAction(..) so that when a client invokes the Action you actually do something:

public object InvokeServiceAction(object dataService, ServiceAction action, object[] parameters)
{
   if (action.Name == "Checkin")
   {
      Movie m = (parameters[0] as IQueryable<Movie>).SingleOrDefault();
      return Checkin(m);
   }
   else if (action.Name == "Checkout")
   {
      Movie m = (parameters[0] as IQueryable<Movie>).SingleOrDefault();
      return Checkout(m);
   }
   else
      throw new NotSupportedException();
}

As you can see, this code figures out which action is being invoked and then gets the binding parameter from the parameters collection. The binding parameter will be an unexecuted query (it is unexecuted because this gives a provider the opportunity to invoke an action without actually retrieving the parameter from the datasource, if indeed that is possible), so we extract the Movie by casting parameters[0] to IQueryable<Movie> and calling SingleOrDefault, and then we call the appropriate code for the action directly.

And we are done…

WARNING: This code will need to change by RTM so that Actions actually get invoked during IDataServiceUpdateProvider.SaveChanges(..). This will involve creating delegates and returning something that isn’t the actual results, but rather something from which you can get the results later. See this post on implementing IDataServiceUpdateProvider for more context if you are interested.

Conclusion:

As you can see, Actions is a work in progress, and many things are likely to change. Even though it is a lot of work to implement actions with the CTP (mainly because you have to implement IDataServiceMetadataProvider2, IDataServiceQueryProvider2 and IDataServiceUpdateProvider2 from scratch), it’s worth trying because Actions opens up the world of behaviors to OData.

Come RTM we expect the whole experience to be a lot better.

Let me know if you have any questions.

Alex James
Program Manager
OData Team


Comments (7)

  1. I don't understand what you say!!

    How do I study??Please!!!

  2. John Schlesinger says:

    The example '/GetMoviesbyGenre' is an RPC. So using actions in this way is not in the least bit RESTful. Actions can be RESTful, but only if they apply to a set of URIs as part of the uniform interface constraint. If a subset of your URIs are for resources that all support the same set of actions for their lifecycle, then actions could be used in a RESTful way. Unfortunately, the example you give is exactly the opposite. Instead of the RPC, a RESTful interface would use GET on /Movies?genre='comedy' which of course doesn't need actions.

  3. Alex James says:

    John,

    I agree the sample isn't RESTful, I didn't claim it was, this is a legacy feature of OData called ServiceOperations, which until this feature were all but inaccessible from the WCF Data Services Client.

    Interestingly when WCF Data Services adds support for Functions, these will be invoked in a similar way, but the saving grace is that the server is in charge, because it gets to tell the client what Functions are available (in the same way it tells what actions are available) where they can be invoked and what additional information is required. This is similar to XForms used in RESTful hypermedia systems, so I feel good about it.

    Alex

  4. Yogev Smila says:

    Hey Alex, thank you and your team for a great solution!

    I have a simple question, is there any way to disable serialization for certain properties on the client side?

    I've managed to remove them from the payload, but I don't want that, I want to them to not be serialized at all.

    Thanks!

  5. Alex,

    Can you tell when Actions support is going to be RTM?

    Thanks!

  6. Can you provide the full source code of the example you mention in "Setting up a WCF Data Service with Actions"? The snippets included in the article give an idea how to do it but it would be better to have a complete example.

  7. Heidi Trimas says:

    What is the status of Actions now? Do we still need to implement these 3 IDataServiceProvider classes?