DynamicsWebApi icon indicating copy to clipboard operation
DynamicsWebApi copied to clipboard

Batch Requests - Opportunity for enhancing documentation.

Open memersond opened this issue 2 years ago • 3 comments

While trying to implement atomic batch operations, I ran into several difficulties. I finally ended up getting it working, but I found 4 inconsistencies that caused confusion for me.

  1. You cannot set returnRepresentation to true inside the requests. I did not see any mention of this being the case and the error message simply said 'Error' when I tried it. Took some trial and error to figure out, but mention of this could help others in the future.

  2. The code example under "Use Content-ID to reference requests in a Change Set" section, shows contentId = 1 on the first record and contentId = $1 in the second record. Setting the contentId this way on the second record threw an error saying the URL could not be parsed. I found what worked for me was: Instead of setting contentId on the second record, I used '$1' as a value on an odata.bind field inside the entity object.

  3. The docs state that only the first response in a batch will be returned due to a CRM limitation. While we cannot get full record info returned due to issue 1 in this post, I found that the GUID values for both records I created in the Batch request were returned.

  4. I can't see a clear distinction between normal batches with multiple separate requests vs. atomic batch operations where requests are rolled back on a failure. Maybe I'm just missing something, but I'm still not quite sure how to separate requests to make them non-atomic.

I apologize if this is not the appropriate area the raise this issue. I'm not sure if the wrong portion is in the docs or the code, so I didn't want to propose any changes without knowing how the code was expected to work.

memersond avatar Apr 30 '22 01:04 memersond

@memersond thank you so much for taking time and writing this issue! this is actually a perfect place to post it. many things could have changed since I have developed and tested the library (regarding CRM limitation and returnRepresentation), so I will need to test the batches again and see if they still work as they worked back then, they could have been patched or got new bugs.

It would be awesome if you could provide samples for all of the cases (where possible), it will be easier for me to test and see what can be improved in the documentation.

Although, many things I do not mention in the library's docs because I assume that the developers would first read and learn everything they can about the batch operations in the official Microsoft Documentation and then come here and try to implement necessary operations using the library, hope that makes sense.

Again, thank you so much and will look at all of the points for sure!

AleksandrRogov avatar Apr 30 '22 17:04 AleksandrRogov

I was not testing on the same data set in the docs, but I would happy to show the code I was using:

    var carePlanRecord = {
        "msemr_title" : "testCarePlanBatch"
    }

    var carePlanRequest = {
        collection: "msemr_careplans",
        entity: carePlanRecord,
        //returnRepresentation: true,
        contentId: '1'
    }

    var carePlanActivityRecord = {
        "statecode": 0,
        "msemr_description": "someText",
        "[email protected]": "$1"
    }

    var carePlanActivityRequest = {
        collection: "msemr_careplanactivities",
        entity: carePlanActivityRecord,
        //returnRepresentation: true,
        //contentId: '$1'
    }

    dynamicsApi.startBatch()

    dynamicsApi.createRequest(carePlanRequest)

    dynamicsApi.createRequest(carePlanActivityRequest)

    var result = await dynamicsApi.executeBatch().catch(function (err){console.log("ERROR:" + err)})

The code is working for me as shown above. The first 2 issues I list can be reproduced by implementing the commented lines in the request objects and/or entity objects.

Number 3 can be observed just by running the working code and printing out 'result'.

On Number 4: I was able to verify that the working code above works in an atomic manner, but still unsure how to do it non-atomically.

I hope this helps. Please let me know if there is anything else I can help with. Thanks for the great work you do on this library.

memersond avatar Apr 30 '22 17:04 memersond

hi @memersond , I have finally found some time to investigate this, sorry for such a big delay. Here are my findings:

  1. You can set returnRepresentation to true in the requests, unless you are trying to link this record with another record in the same request. Thus, returnRepresentation: true will work in your example if you comment out "[email protected]": "$1" in the entity record for the 2nd request (Note, that in that case the records won't be related to each other). This seems to be a Dynamics 365 limitation (or maybe REST Api limitation), which they do not describe in their documentation either, but I will mention it in mine. Good catch!

P.S., returnRepresentation will work just fine if you add it in the 2nd request, because you are not linking that record to anything else. Let me know otherwise! I tested that with different requests, but similar to yours.

  1. When you set contentId: "$1", DynamicsWebApi treats it like a replacement for the URL request and all of other parameters are added at the end of that URL, for example if you provide a collection in the same request it will be: $1/collection. It works almost like in this official example. In that example they are doing the following: 1. create a record A, then 2. create a record B and then 3. attach B to A. The same can be done with DynamicsWebApi:
const request3 = {
    collection: "$1",
    entity: {
        "[email protected]": "$2"
    }
}

In your example it did not work, because it was trying to replace the URL for the 2nd request with the URL returned in the 1st one and add the collection name at the end, it was probably become something like this: <api url>/msemr_careplans(<guid>)/msemr_careplanactivities and, of course, that URL does not exist in the system. I understand that it is confusing, so I will include an explanation in the documentation (maybe even an additional parameter, we will see). The contentId: "$1" would work for you if it was done this way:

//create the activity first
const carePlanActivityRecord = {
    "statecode": 0,
    "msemr_description": "someText",
    //"[email protected]": "$1" <- we do not set this, because the care plan does not exist yet
}

const carePlanActivityRequest = {
    collection: "msemr_careplanactivities",
    entity: carePlanActivityRecord,
    contentId: '1'
}
//and the care plan second
const carePlanRecord = {
    "msemr_title" : "testCarePlanBatch"
}

const carePlanRequest = {
    collection: "msemr_careplanactivity_msemr_careplan", // <- I am guessing here, but this should be a navigation property that links these 2 entities (1:N or N:1 relationships only!)
    entity: carePlanRecord,
    returnRepresentation: true, //<- this will work here
    contentId: '$1'
}

dynamicsApi.startBatch();
dynamicsApi.createRequest(carePlanActivityRequest); //creating an activity first
dynamicsApi.createRequest(carePlanRequest); //then creating a plan and attach it at the same time

//in this case the url for the 2nd request would be a correct one:
//<api url>/msemr_careplanactivities(guid)/msemr_careplanactivity_msemr_careplan
//again, I am guessing with the navigation property, because it may be named differently

const result = await dynamicsApi.executeBatch().catch(function (err){console.log("ERROR:" + err)})

My example in the docs works fine, because the collection parameter is being treated as a navigation property rather than a collection (the following line is from the docs):

dynamicsWebApi.createRequest({ entity: contact, collection: 'customerid_contact', contentId: '$1' });

You can see here that the "collection" parameter is customerid_contact, which is an OOTB navigation property that links Account with a Contact: "Parent Customer".

  1. This needs an explanation in the docs from me as well. The limitation that I am talking about in the Docs applied only if the response that you are trying to get comes from the request to a Navigation Property. For some reason, Dynamics 365 does not return a response in that case. In all other cases the response will be there.
  2. An official documentation to Web Api says the following: A batch request can include GET requests and change sets.

I initially interpreted this line that the POST, PATCH, PUT and DELETE requests must be a change set. Thus, DynamicsWebApi automatically converts any sequential request (that is not a GET) into a change set. Now, after your comment, I realize that this may not be the case in all situations and I will need to test those requests as atomic operations. If it is possible to do sequential POST, PATCH, PUT and DELETE without including them into a change set - I will need to implement that in one of the future patches.

If you have any question, please let me know but I hope that this helps.

Again, I am sorry that it took me so long to get back to this issue and thank you for using the library!

AleksandrRogov avatar Jul 15 '22 20:07 AleksandrRogov

Closing this due to inactivity. If you have any question about batch requests, you can still message here.

AleksandrRogov avatar Aug 20 '22 13:08 AleksandrRogov