Problem with Web API OData batch operations with relative URIs

Topics: ASP.NET Web API
Nov 4, 2013 at 5:55 AM
I think this may be a bug, but I'm not sure.

Basically I'm trying to submit an OData batch operation, but it's not calling my controller action. There are three components involved in this:

1) Web API OData
2) ODataLib
3) BreezeJS (which uses datajs for client OData support)

BreezeJS by default generates the batch sub-operations (e.g. POST/PUT) using a URI relative to the odata route prefix. For example, for a service hosted at http://localhost:1234/odata, invoking saveChanges() for a batch that includes one insert sends the following multipart requerst:
--batch_e3f8-a5e8-e8cc
Content-Type: multipart/mixed; boundary=changeset_f02f-91e5-10d3

--changeset_f02f-91e5-10d3
Content-Type: application/http
Content-Transfer-Encoding: binary

POST Topics HTTP/1.1
Content-ID: 1
DataServiceVersion: 2.0
Accept: application/atomsvc+xml;q=0.8, application/json;odata=fullmetadata;q=0.7, application/json;q=0.5, */*;q=0.1
Content-Type: application/json
MaxDataServiceVersion: 3.0

{"Name":"New Topic"}
--changeset_f02f-91e5-10d3--

--batch_e3f8-a5e8-e8cc--
If I change the POST URI in Fiddler to be absolute, it works. First I thought this is a problem in BreezeJS itself, but when I read the OData spec it doesn't seem to be very clear on whether the batch sub-operation URIs should be relative or absolute.

A similar issue was first repoted in http://stackoverflow.com/questions/18813890/post-batch-request-with-breezejs.

Oh and I'm using CORS btw (the Web API is in a different application from the client app), but I don't think this is related to the problem.

I had to dig into the ASP.NET Web API OData source code to see if relative URIs are supported. I found that it actually tries to build the operation URI by combining the OData route base URI with the operation relative URI, but something interesting happens deep down in one of the ODataLib methods (in the Microsoft.Data.OData assemblly):
internal static Uri UriToAbsoluteUri(Uri baseUri, Uri relativeUri)
{
    ...
    return new Uri(baseUri, relativeUri);
}
The problem is that the baseUri in this case is http://localhost:1234/odata and the relativeUri is Topics. Without a trailing slash in the baseUri, this will result in the following output Uri http://localhost:1234/Topics. If there was a trailing slash we would get the desired result http://localhost:1234/odata/Topics.

Now I'm not sure who's responsibility is to fix this:

1) BreezeJS should use absolute URIs in sub-operations, e.g. /odata/Topics
2) Web API OData should use a baseUri with a trailing slash, so that it can be combined properly with relativeUris
3) ODataLib should be smart about combining baseUris and relativeUris so as to avoid the above trailing slash issue

Or it could be something else that I"m not aware of. I just thought I'd post this in a discussion thread first to gather feedback before submitting a bug report.

Thanks,
Khaled
Dec 4, 2013 at 5:36 PM
1) breeze.js should use absolute URIs.
If the specification is unclear about whether you should use relative URIs or not, then always an absolute path is a more specific and more reliable.

2) WebApi should use a base path with a trailing slash
I think not. Trailing slash would designate a directory rather than an endpoint.

3) ODataLib should be smart about combining baseUris and relativeUris so as to avoid the above trailing slash issue
Most certainly yes. From your description, it definitely looks like a bug. Besides, this exact same problem happens also with JayData, not just breeze. To overcome the issue, I created a custom Batch Handler as described here: http://jaydata.org/forum/viewtopic.php?f=3&t=300
Dec 4, 2013 at 8:24 PM
"BreezeJS by default generates the batch sub-operations (e.g. POST/PUT) using a URI relative to the odata route prefix"

This is not correct. Breeze prefixes the URL with whatever you supply as the service name. If you supply a relative path, that's what breeze uses. If you supply the absolute path, breeze will use that. Here is the pertinent code from the Breeze DataService class
proto.makeUrl = function(suffix) {
        var url = this.serviceName;
        // remove any trailing "/"
        if (core.stringEndsWith(url, "/")) {
            url = url.substr(0, url.length - 1);
        }
        // ensure that it ends with "/" + suffix
        suffix = "/" + suffix;
        if (!core.stringEndsWith(url, suffix)) {
            url = url + suffix;
        }
        return url;
    };
More precisely, Breeze constructs the URL from the material you give it and passes that along to datajs which handles the communication with the server. Here are the applicable lines from the saveChanges method of the OData service adapter:
ctor.prototype.saveChanges = function (saveContext, saveBundle) {

    ...
    var url = saveContext.dataService.makeUrl("$batch");
    ....
    OData.request({
        headers : { "DataServiceVersion": "2.0" } ,
        requestUri: url,
        method: "POST",
        data: requestData
    },
The choice is yours.
Dec 13, 2013 at 6:44 AM
Ward: Thanks for clarifying that aspect of Breeze.

I think what you described is true of the top-level batch request (that goes to the batch handler at e.g. /odata/$batch), as well as any non-batch requests. However, I was talking about the request URIs in the batch sub-operations. In the multi-part request example in my original post, the sub-operation URI is Topics, not /odata/Topics.

The pertinent part in Breeze that constructs those sub-operation URIs is in the createChangeRequests function:
    function createChangeRequests(saveContext, saveBundle) {
        ...
            if (aspect.entityState.isAdded()) {
                request.requestUri = entity.entityType.defaultResourceName;
                request.method = "POST";
                request.data = helper.unwrapInstance(entity, true);
                tempKeys[id] = aspect.getKey();
            } else if (aspect.entityState.isModified()) {
                updateDeleteMergeRequest(request, aspect, prefix);
                request.method = "MERGE";
                request.data = helper.unwrapChangedValues(entity, entityManager.metadataStore, true);
                // should be a PATCH/MERGE
            } else if (aspect.entityState.isDeleted()) {
                updateDeleteMergeRequest(request, aspect, prefix);
                request.method = "DELETE";
            } else {
                return;
            }
            changeRequests.push(request);
        ...
    }

    function updateDeleteMergeRequest(request, aspect, prefix) {
        ...
        var uri = extraMetadata.uri || extraMetadata.id;
        if (__stringStartsWith(uri, prefix)) {
            uri = uri.substring(prefix.length);
        }
        request.requestUri = uri;
        ...
    }
So for POST sub-operations, the URI used is the entity.entityType.defaultResourceName, which is a relative URI that does not include the serviceName. For MERGE and DELETE sub-operations, the code explicitly strips out the serviceName part of the URI, leaving only the relative part.
Dec 31, 2013 at 2:46 AM
Hi Khaledh

just had the same thing .. spent about an hour tracking it down :-(

in breeze.debug.js I changed line 15164 (function createChangeRequests)

from
request.requestUri = entity.entityType.defaultResourceName;

to
request.requestUri = prefix + entity.entityType.defaultResourceName;

which seems to make it work .. haven't fully tested as it's 02:45 and I need some sleep ;-)

cheers
Stu
Dec 31, 2013 at 3:11 AM
Edited Dec 31, 2013 at 3:14 AM
@stooboo Yes, that's what I did as a temp fix as well a while ago (now I use a different workaround, see below). I submitted a pull request to Breeze a couple of months ago that includes this "fix". I'm not entirely comfortable with having the full URL prefix in there including the hostname - should be just the absolute path to the resource without the hostname part I guess.

Also I think ODataLib should be fixed to combine baseUris and relativeUris properly (I haven't submitted a bug yet).

Eventually to avoid this problem altogether, I just dropped my WebApi odata route prefix when setting up my odata route:
config.Routes.MapODataRoute(
    routeName: "odata",
    routePrefix: "",
    model: edmModel,
    ...
);
This essentially makes my odata relative and absolute URIs one and the same, thus avoiding this whole problem. Not what I hoped to do, but it was the simplest approach I can do without changing any of the underlying libraries.

Keep in mind that my WebApi is in a separate probject, so this route is the only route in this api project. If you host your web site and api in the same project then this may cause issues in routing.
Dec 31, 2013 at 3:48 PM
Great info - very clear - cheers khaledh

Stu