“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
0 comments