共用方式為


Actions in WCF Data Services – Part 1: Service Author Code

If you read our last post on Actions you’ll know that Actions are now in both OData and WCF Data Services and that they are cool:

“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."

Our key design goal for RTM was to allow you or third party vendors to create an Action Provider that adds Action support to an existing WCF Data Services Provider. Adding Actions support to the built-in Entity Framework Provider for example.

This post is the first of a series that will in turn introduce:

  1. The Experience we want to enable, i.e. the code the service author writes.
  2. The Action Provider API and why it is structured as it is.
  3. A sample implementation of an Action Provider for the Entity Framework

Remember if you are an OData Service author, happy to use an Action Provider written by someone else, all you need worry about is (1), and that is what this post will cover.

Scenario

Imagine that you have the following Entity Framework model

public class MoviesModel: DbContext
{
public DbSet<Actor> Actors { get; set; }
public DbSet<Director> Directors { get; set; }
public DbSet<Genre> Genres { get; set; }
public DbSet<Movie> Movies { get; set; }
public DbSet<UserRental> Rentals { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<UserRating> Ratings { get; set; }
}

Which you expose as using WCF Data Services configured like this:

config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);
config.SetEntitySetAccessRule("EdmMetadatas", EntitySetRights.None);
config.SetEntitySetAccessRule("Users", EntitySetRights.None);
config.SetEntitySetAccessRule("Rentals", EntitySetRights.None);
config.SetEntitySetAccessRule("Ratings", EntitySetRights.None);

Notice that some of the Entity Framework data is completely hidden (Users,Rentals,Ratings and of course EdmMetadatas) and the rest is marked as ReadOnly. This means in this service people can see information about Movies, Genres, Actors, Directors and Tags, but they currently can’t edit this data through the service. Essentially some of the database is an implementation detail you don’t want the world to see. With Action for the first time it is easy to create a real distinction between your Data Model and your Service Model.

Now imagine you have a method on your DbContext that looks something like this:

public void Rate(Movie movie, int rating)
{
var userName = GetUsername();
var movieID = movie.ID;

    // Retrieve the existing rating by this user (or null if not found).
    var existingRating = this.Ratings.SingleOrDefault(r =>
r.UserUsername == userName &&
r.MovieID == movieID
);

    if (existingRating == null)
{
this.Ratings.Add(new UserRating { MovieID = movieID, Rating = rating, UserUsername = userName });
}
else
{
existingRating.Rating = rating;
}
}

This code allows a user to rate a movie, simply by providing a rating and a movie. It uses ambient context to establish who is making the request (i.e. HttpContext.Current.User.Identity.Name), and looks for a rating by that User for that Movie (a user can only rate a movie once), if it finds one it gets modified otherwise a new rating is created.

Target Experience

Now imagine you want to expose this as an action. The first step would be to make your Data Service implement IServiceProvider like this:

public class MoviesService : DataService<MoviesModel>, IServiceProvider
{
public object GetService(Type serviceType)
{
if (typeof(IDataServiceActionProvider) == serviceType)
{
return new EntityFrameworkActionProvider(CurrentDataSource);
}
return null;
}
    …
}

Where EntityFrameworkActionProvider is the custom action provider you want to use, in this case a sample provider that we’ll put up on codeplex when Part 3 of this series is released (sorry to tease).

Next configure your service to expose Actions, in the InitializeService method:

config.SetServiceActionAccessRule("*", ServiceActionRights.Invoke);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;

As per usual * means all Actions, but you could just as easy expose just the actions you want by name.

Finally with that groundwork done, expose your action by adding the [Action] attribute:

[Action]
public void Rate(Movie movie, int rating)
{

With this done the action will show up in $metadata, like this:

<FunctionImport Name="Rate" IsBindable="true">

   <Parameter Name="movie" Type="MoviesWebsite.Models.Movie" />

   <Parameter Name="rating" Type="Edm.Int32" />

</FunctionImport>
And it will be advertised in Movie entities like this (in JSON format):

{
"d": {
"__metadata": {

"actions": {
"https://localhost:15238/MoviesService.svc/$metadata#MoviesModel.Rate": [
{
"title": "Rate",
"target": "https://localhost:15238/MoviesService.svc/Movies(1)/Rate"
}
]
}
},

"ID": 1,
"Name": "Avatar"
}
}

This action can be invoked using the WCF Data Services Client like this:

ctx.Execute(
new Uri(“https://localhost:15238/MoviesService.svc/Movies(1)/Rate”),
“POST”,
new BodyParameter(“rating”,4)
);

or, if you prefer being closer to the metal, more directly like this:

POST https://localhost:15238/MoviesService.svc/Movies(1)/Rate HTTP/1.1
Content-Type: application/json
Content-Length: 20
Host: localhost:15238

{
"rating": 4
}

All very easy don’t you think?

Actions whose availability depends upon Entity State

You may also remember that whether an action is available can depend on the state of the entity or service. For example you can’t Checkout a movie that you already have Checked out. To address this requirement we need a way to support this through attributes too.

Excuse the terrible attribute name (it is only a sample) but perhaps something like this:

[OccasionallyBindableAction("CanCheckout")]
public void Checkout(Movie movie)
{
var userName = GetUsername();
var movieID = movie.ID;

    var alreadyCheckedOut = this.Rentals.Any(r =>
r.UserUsername == userName &&
r.MovieID == movieID &&
r.CheckedIn == null
);

    if (!alreadyCheckedOut)
{
// add a new rental
this.Rentals.Add(
new UserRental {
MovieID = movieID,
CheckedOut = DateTime.Now,
UserUsername = userName
}
);
}
}

Notice instead of the [Action] attribute this has an [OccasionallyBindableAction] attribute which takes the name of a method to call to see if this action is available. This method must take one parameter, the same type as the binding parameter of the action (so in this case a Movie) and return a boolean to indicate whether the action should be advertised.

[SkipCheckForFeeds]
public bool CanCheckout(Movie movie)
{
var username = GetUsername();
var rental = this.Rentals.SingleOrDefault(
r => r.UserUsername == username && r.MovieID == movie.ID && r.CheckedIn == null
);
if (rental == null) return true;
else return false;
}

Notice that this method runs a separate query to see if an unreturned rental exists for the current user and movie, if yes then the movie can’t be checked out, if no then the movie can be checked out.

The alert amongst you will have noticed the [SkipCheckForFeeds] attribute. This is here for performance reasons. Why?

Imagine that you retrieve a Feed of Movies, in theory we should call this ‘availability check’ method for every Movie in the feed. Now if the method only needs information in the Movie (i.e. imagine if the Movie has a CanCheckout property) this is not a problem, but in our example we actually issue a separate database query. Clearly running one query to get a feed of items and then a new query for each action in that feed of items is undesirable from a performance perspective.

We foresaw this problem and added some rules to the protocol to address this problem, namely if it is expensive to calculate the availability of an Action, it is okay to advertise the action whether it is available or not. So [SkipCheckForFeed] is used to indicate to our custom Actions Provider that this method is expensive and shouldn’t be called for all the Movies in a feed but should be advertised anyway.

Summary

As you can see our goal is to enable 3rd party Action Provider to provide a highly intuitive way of adding Actions to your OData Service, the above code examples illustrate one possible set of experiences. As you experiment I’m confident that you will find Actions to be a very powerful way to add behaviors to your OData Service.

In Part 2 we will look at the IDataServiceActionProvider interface in detail, and then in Part 3 we’ll walk through an implementation for the Entity Framework (and I’ll post the sample Entity Framework Action Provider sample code).

As always I’m super keen to hear what you think.

Alex James
Senior Program Manager, Microsoft.

Comments

  • Anonymous
    April 12, 2012
    What is the current version beyond RTM

  • Anonymous
    April 23, 2012
    hi, i implement a linq2sql provider. how can i (easily?) expose actions through the new DataService class? i have table/view access happening.... thanks, o

  • Anonymous
    April 25, 2012
    The comment has been removed

  • Anonymous
    April 26, 2012
    The comment has been removed