ODataActions sample - interpreting generated metadata for CheckOut and CheckOutMany

Topics: ASP.NET Web API
Jul 24, 2013 at 4:02 AM
Hi,

I've posted a fairly detailed question @ StackOverflow - http://stackoverflow.com/questions/17803465/webapi-odata-actions-sample-differentianting-between-checkout-and-checkoutmany.

Basically I'm most of the way through making a T4 template that will generate a client-side context for me that has methods for invoking OData actions with appropriate parameters, etc.

However I would like to better understand how I can tell solely from the generated $metadata how I should call CheckOutMany (where it seems I should ignore the bindable parameter in the edmx) versus CheckOut (the overload that accepts $filter, which is via the bindable parameter of type Movie).

I'd really appreciate feedback from anyone knowledgeable in the area to point me in the right direction. At the moment I'm thinking of a heuristic to know when to ignore the bindable parameter in CheckOutMany - if the bindable parameter is a collection of an entity E, and there's another parameter which is a collection of the type of E's single key, then ignore the bindable parameter... That sounds a little flaky though.

I'm using nightly builds with the ODataActionsSample - the nightly build might've been acquired late last week but I haven't seen much change in the commit history that would affect this. (very happy to be told I'm wrong though!)

EDMX
        <FunctionImport Name="CheckOut" ReturnType="Collection(ODataActionsSample.Models.Movie)" IsBindable="true" EntitySet="Movies" m:IsAlwaysBindable="true">
          <Parameter Name="bindingParameter" Type="Collection(ODataActionsSample.Models.Movie)" Nullable="false" />
        </FunctionImport>
        <FunctionImport Name="CheckOutMany" ReturnType="Collection(ODataActionsSample.Models.Movie)" IsBindable="true" EntitySet="Movies" m:IsAlwaysBindable="true">
          <Parameter Name="bindingParameter" Type="Collection(ODataActionsSample.Models.Movie)" Nullable="false" />
          <Parameter Name="MovieIDs" Type="Collection(Edm.Int32)" Nullable="false" />
        </FunctionImport>

WebApiConfig.cs extract

            // CheckOut action
            // URI: ~/odata/Movies/CheckOut
            // Shows how to bind to a collection, instead of a single entity.
            // This action also accepts $filter queries. For example:
            //     ~/odata/Movies/CheckOut?$filter=Year eq 2005
            var checkOutFromCollection = modelBuilder.Entity<Movie>().Collection.Action("CheckOut");
            checkOutFromCollection.ReturnsCollectionFromEntitySet<Movie>("Movies");

            // CheckOutMany action
            // URI: ~/odata/Movies/CheckOutMany
            // Shows an action that takes a collection parameter.
            ActionConfiguration checkoutMany = modelBuilder.Entity<Movie>().Collection.Action("CheckOutMany");
            checkoutMany.CollectionParameter<int>("MovieIDs");
            checkoutMany.ReturnsCollectionFromEntitySet<Movie>("Movies");
MoviesController.cs method extracts

        // Check out a list of movies.
        [HttpPost]
        public ICollection<Movie> CheckOutMany(ODataActionParameters parameters)
        {
            if (!ModelState.IsValid)
            {
                throw new HttpResponseException(HttpStatusCode.BadRequest);
            }

            // Client passes a list of movie IDs to check out.
            var movieIDs = parameters["MovieIDs"] as ICollection<int>;

            // Try to check out each movie in the list.
            var results = new List<Movie>();
            foreach (Movie movie in db.Movies.Where(m => movieIDs.Contains(m.ID)))
            {
                if (TryCheckoutMovie(movie))
                {
                    results.Add(movie);                    
                }
            }

            try
            {
                db.SaveChanges();
            }
            catch (DbUpdateConcurrencyException)
            {
                throw new HttpResponseException(HttpStatusCode.BadRequest);
            }

            // Return a list of the movies that were checked out.
            return results;
        }

        [HttpPost]
        // This action accepts $filter queries. For example:
        //     ~/odata/Movies/CheckOut?$filter=Year eq 2005
        public ICollection<Movie> CheckOut(ODataQueryOptions opts)
        {
            // Validate the query options.
            var settings = new ODataValidationSettings()
            {
                AllowedQueryOptions = AllowedQueryOptions.Filter
            };
            opts.Validate(settings);

            // Use the query options to get a filtered list of movies.
            var movies = opts.ApplyTo(db.Movies) as IQueryable<Movie>;

            // Try to check out each movie in the list.
            var results = new List<Movie>();
            foreach (Movie movie in movies)
            {
                if (TryCheckoutMovie(movie))
                {
                    results.Add(movie);
                }
            }

            try
            {
                db.SaveChanges();
            }
            catch (DbUpdateConcurrencyException)
            {
                throw new HttpResponseException(HttpStatusCode.BadRequest);
            }

            // Return a list of the movies that were checked out.
            return results;
        }
Thanks for reading!

Cheers,
Ian
Jul 24, 2013 at 4:05 AM
Also, not that it would affect whether or not I get help, but I'm hoping to release the T4 template as my first open source contribution somehow once it's working and I've made use of it for a few weeks. I've looked high & low for something to do the code generation for WebAPI calls or OData actions calls without much luck. There were a few projects around the place to make T4-based proxies for WebAPI calls - some for javascript and some for C# - but they didn't work terribly well and had no concept of OData.
Jul 24, 2013 at 9:52 AM
I've been working through this more - here's my comment on my post from SO.
Looking in to this more, because CheckOutMany is bound to the Movies collection it's still quite legitimate to pass through an OData filter and access this from within CheckOutMany's server-side code using the QueryOptions property on the ODataController.  I suppose the example should really have been that CheckOutMany was NOT bound to the Movies collection because the action, whilst accepting an OData filter, actually does nothing with it.  I've modified the sample source to respect the given filter.
Basically I guess the sample should be adjusted because, for me at least, CheckOutMany led me to believe it was "special" when really the MovieIDs parameter could've been a set of actorIDs to be attached to movies where the movies were to come via a filter expression. Perhaps CheckoutMany should be changed to not be bound to the Movies collection in the sample?