Search code examples
odatabreezeasp.net-web-api

Post batch request with Breezejs


I've been trying to POST an entity using Breezejs and WebAPI OData Controllers.
Here are the configurations:

config.Routes.MapODataRoute(
routeName: "odata",
routePrefix: "odata",
model: model,
batchHandler: new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer));

Where the model is very straight forward:

public class ServiceMetadata
{
    public int ServiceMetadataId { get; set; }
    public string ServiceName { get; set; }
    public string Description { get; set; }
    public ObjectState? State { get; set; }
    public DateTime? LastUpdated { get; set; }
}

And it is mapped through the default:

ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();

The Client is also very simple taken using AngularJs and partially from the Todo example: http://www.breezejs.com/samples/todo-angular

breeze.config.initializeAdapterInstance("modelLibrary", "backingStore", true);
var serviceName = 'http://localhost:8081/odata/';
breeze.config.initializeAdapterInstances({ dataService: "OData" });
var manager = new breeze.EntityManager(serviceName);
manager.enableSaveQueuing(true);

The actual Posting is done using the default createEntity() method:

function createServiceMetadata(initialValues) {
    return manager.createEntity('ServiceMetadata', initialValues);
}

And the whole thing looks like:

 serviceMetadatas.createServiceMetadata({
     ServiceName: $scope.newServiceName,
     Description: $scope.newServiceDescription
 });

 serviceMetadatas.saveChanges();


However, the request is not being transferred to the correct controller (ServiceMetadatasController which inherits from EntitySetController), or any other controller for that matter.
The HTTP request looks like this:

    POST http://localhost:8081/odata/$batch HTTP/1.1
Host: localhost:8081
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; rv:23.0) Gecko/20100101 Firefox/23.0
Accept: multipart/mixed
Accept-Language: he-IL,he;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
DataServiceVersion: 2.0
Content-Type: multipart/mixed; charset=UTF-8;boundary=batch_4f09-d7cf-dd99
MaxDataServiceVersion: 2.0
Referer: http://localhost:9000/
Content-Length: 580
Origin: http://localhost:9000
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache


--batch_4f09-d7cf-dd99
Content-Type: multipart/mixed; boundary=changeset_ca0c-06b7-ddbe

--changeset_ca0c-06b7-ddbe
Content-Type: application/http
Content-Transfer-Encoding: binary

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

{"ServiceMetadataId":-1,"ServiceName":"sdf sdf","Description":"sd fgs df","LastUpdated":null}
--changeset_ca0c-06b7-ddbe--

--batch_4f09-d7cf-dd99--

And the response:

    HTTP/1.1 202 Accepted
Cache-Control: no-cache
Pragma: no-cache
Content-Type: multipart/mixed; boundary=batchresponse_966d4460-e00e-4900-b1c9-85b17081cfac
Expires: -1
Server: Microsoft-IIS/8.0
Access-Control-Allow-Origin: http://localhost:9000
Access-Control-Allow-Credentials: true
DataServiceVersion: 2.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcVG9tZXJcRG9jdW1lbnRzXFZpc3VhbCBTdHVkaW8gMjAxMlxQcm9qZWN0c1xFYXN5Qml6eVxFYXN5Qml6eS5XZWJBUElcb2RhdGFcJGJhdGNo?=
X-Powered-By: ASP.NET
Date: Sun, 15 Sep 2013 14:32:39 GMT
Content-Length: 443

--batchresponse_966d4460-e00e-4900-b1c9-85b17081cfac
Content-Type: multipart/mixed; boundary=changesetresponse_44da5dcf-877d-4041-a82b-c51d06a4e9a4

--changesetresponse_44da5dcf-877d-4041-a82b-c51d06a4e9a4
Content-Type: application/http
Content-Transfer-Encoding: binary

HTTP/1.1 406 Not Acceptable
Content-ID: 1


--changesetresponse_44da5dcf-877d-4041-a82b-c51d06a4e9a4--
--batchresponse_966d4460-e00e-4900-b1c9-85b17081cfac--

Any idea what the hack is going on? B.T.W GET requests works great.

P.S. After looking at couple of demos, I though the using BreezeJS will be straight forward considering WebApi and OData.
I must say it is Far from being easy to configure this JS library. I hope it will turn out to be hard-to-setup but easy-to-use.

Thanks.

@UPDATE See Javier's great answer!!

In after digging allllot on the breeze code, I came to realize that the problem is laying deep in the createChangeRequests() of breezejs, right here:

request.requestUri = entity.entityType.defaultResourceName;

Where for some reason the defaultResouceName, completely ignores the path to this entity. Long story short, the following is a hack to resolve:

manager.metadataStore.getEntityType(ENTITY_TYPE).setProperties({defaultResourceName: THE_MISSING_PART_FROM_THE_URL + ENTITY_TYPE});
manager.createEntity(ENTITY_TYPE, values);

Not very nice, but still works!


Solution

  • The problem is in the url of the inner request. The url needs to be relative to the host. Let's say your service is hosted in host/service (in our case, service will be the equivalent as the odata prefix), so normally you send requests like host/service/Customers or /service/Customers.

    When you issue a batch request, the urls in the inner requests might be absolute or relative to the host. The problem is that in your request, the url is ServiceMetadatas which is relative to the service root, not the host.

    Web API is interpreting the relative url as host/ServiceMetadatas instead of as host/service/ServiceMetadatas and that's what causes the error.

    Based on your repro project, the following request works fine:

    POST http://localhost:6974/odata/$batch HTTP/1.1
    Host: localhost:6974
    User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; rv:23.0) Gecko/20100101 Firefox/23.0
    Accept: multipart/mixed
    Accept-Language: he-IL,he;q=0.8,en-US;q=0.5,en;q=0.3
    Accept-Encoding: gzip, deflate
    DataServiceVersion: 2.0
    Content-Type: multipart/mixed; charset=UTF-8;boundary=batch_4f09-d7cf-dd99
    MaxDataServiceVersion: 2.0
    Referer: http://localhost:9000/
    Content-Length: 565
    Origin: http://localhost:9000
    Connection: keep-alive
    Pragma: no-cache
    Cache-Control: no-cache
    
    --batch_4f09-d7cf-dd99
    Content-Type: multipart/mixed; boundary=changeset_ca0c-06b7-ddbe
    
    --changeset_ca0c-06b7-ddbe
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    POST odata/ServiceMetadatas HTTP/1.1
    Content-ID: 1
    DataServiceVersion: 2.0
    Accept: application/atomsvc+xml;q=0.8, application/json;odata=verbose;q=0.5, */*;q=0.1
    Content-Type: application/json;odata=verbose
    MaxDataServiceVersion: 2.0
    
    {"ServiceMetadataId":-1,"ServiceName":"sdf sdf","Description":"sd fgs df"}
    --changeset_ca0c-06b7-ddbe--
    
    --batch_4f09-d7cf-dd99--
    

    The associated response is the following one:

    HTTP/1.1 202 Accepted
    Cache-Control: no-cache
    Pragma: no-cache
    Content-Type: multipart/mixed; boundary=batchresponse_6779b5e5-6e40-4363-9a98-5a33d062da28
    Expires: -1
    Server: Microsoft-IIS/8.0
    DataServiceVersion: 2.0
    X-AspNet-Version: 4.0.30319
    X-SourceFiles: =?UTF-8?B?QzpcVXNlcnNcamFjYWx2YXJcRG93bmxvYWRzXE9EYXRhQmF0Y2gtbWFzdGVyXENsZWFuV2ViQXBpUHJvamVjdFxvZGF0YVwkYmF0Y2g=?=
    X-Powered-By: ASP.NET
    Date: Tue, 17 Sep 2013 16:48:50 GMT
    Content-Length: 872
    
    --batchresponse_6779b5e5-6e40-4363-9a98-5a33d062da28
    Content-Type: multipart/mixed; boundary=changesetresponse_b63ca946-ce66-43e6-a78f-d44a5b8f2d5c
    
    --changesetresponse_b63ca946-ce66-43e6-a78f-d44a5b8f2d5c
    Content-Type: application/http
    Content-Transfer-Encoding: binary
    
    HTTP/1.1 201 Created
    Location: http://localhost:6974/odata/ServiceMetadatas(-1)
    Content-ID: 1
    Content-Type: application/json; odata=verbose; charset=utf-8
    DataServiceVersion: 2.0
    
    {
      "d":{
        "__metadata":{
          "id":"http://localhost:6974/odata/ServiceMetadatas(-1)","uri":"http://localhost:6974/odata/ServiceMetadatas(-1)","type":"CleanWebApiProject.Models.ServiceMetadata"
        },"ServiceMetadataId":-1,"ServiceName":"sdf sdf","Description":"sd fgs df"
      }
    }
    --changesetresponse_b63ca946-ce66-43e6-a78f-d44a5b8f2d5c--
    --batchresponse_6779b5e5-6e40-4363-9a98-5a33d062da28--
    

    The only change that I made in the controller is the following (and not related to batch):

    public class ServiceMetadatasController : EntitySetController<ServiceMetadata, int>
    {
        protected override ServiceMetadata CreateEntity(ServiceMetadata entity)
        {
            return entity;
        }
    
        protected override int GetKey(ServiceMetadata entity)
        {
            return entity.ServiceMetadataId;
        }
    
        public override IQueryable<ServiceMetadata> Get()
        {
            return new List<ServiceMetadata>
                   {
                       new ServiceMetadata() {ServiceName = "Service1", Description = "Desc1"},
                       new ServiceMetadata() {ServiceName = "Service2", Description = "Desc1"}
    
                   }.AsQueryable();
        }
    
    }
    

    I hope this solves your problem, also let me know if you are generating the url for the inner request manually or if it's breezejs doing it for you, so that I can follow up and make sure it gets fixed.