3
votes

A follow-question to my previous post about Azure Functions. I need to update a document in DocumentDB using the imperative binder (Binder). I don't really understand the documentation and I can't find any examples (I more or less find one kind of example which is the TextWriter one). The documentation says I can bind to "out T" by I find no examples of this.

Say that the document looks like this before running the function:

{
    child: {
        value: 0
    }
}

And the functions looks like this:

var document = await binder.BindAsync<dynamic>(new DocumentDBAttribute("myDB", "myCollection")
{
    ConnectionStringSetting = "my_DOCUMENTDB",
    Id = deviceId
});

log.Info($"C# Event Hub trigger function processed a message: document: { document }");

document.value = 100;
document.child.value = 200;

log.Info($"Updated document: { document }");

According to the second logging row, the document isn't properly updated. The child is not updated (which existed when read from the store) and value is added. Either way, nothing is persisted. I've tried adding an Output in the function.json, but the compiler complains about it and the documentation states that you shouldn't have any.

What am I missing?

2
@mathewc mentioned below that this scenario won't auto-update for you today. But can you share what your function.json looked like when you saw the compiler errors? I believe you should be able to use an output binding to perform the update -- you'd have to use IAsyncCollector<dynamic> collector as your parameter, then you'd call collector.AddAsync(document). I'll try to set up a sample and post it belowbrettsam
The compilation error was that the output was missing in the Run signature as an out argument, which is as expected. But I tried it according to this blog post: weblogs.asp.net/sfeldman/… where he seem to declare an output without having it as an out argument.Kimmen

2 Answers

5
votes

Mathew's sample (using DocumentClient) works, but I wanted to clarify the other way you can do it with Binder and an output binding.

You're bumping into two issues:

  1. The Document implementation of dynamic appears to return a new object instance every time you request a child object. This isn't related to Functions, but explains why document.child.value = 200 doesn't work. You are updating one instance of child that is not actually attached to the document. I'll try to double-check this with DocumentDb folks, but that is confusing. One way around this is to request a JObject instead of a dynamic. My code below does that.

  2. As @mathewc pointed out, Binder does not auto-update the document. We'll track that in the issue he filed. Instead, you can use an output binding with IAsyncCollector<dynamic> to update the document. Behind-the-scenes we'll call InsertOrReplaceDocumentAsync, which will update the document.

Here's a full sample that worked for me:

Code:

#r "Microsoft.Azure.WebJobs.Extensions.DocumentDB"
#r "Newtonsoft.Json"

using System;
using Newtonsoft.Json.Linq;

public static async Task Run(string input, Binder binder, IAsyncCollector<dynamic> collector, TraceWriter log)
{        
    string deviceId = "0a3aa1ff-fc76-4bc9-9fe5-32871d5f451b";
    dynamic document = await binder.BindAsync<JObject>(new DocumentDBAttribute("ItemDb", "ItemCollection")
    {
        ConnectionStringSetting = "brettsamfunc_DOCUMENTDB",
        Id = deviceId
    });

    log.Info($"C# Event Hub trigger function processed a message: document: { document }");

    document.value = 100;
    document.child.value = 200;

    await collector.AddAsync(document);
    log.Info($"Updated document: { document }");
}

binding:

{
  "type": "documentDB",
  "name": "collector",
  "connection": "brettsamfunc_DOCUMENTDB",
  "direction": "out",
  "databaseName": "ItemDb",
  "collectionName": "ItemCollection",
  "createIfNotExists": false
}
2
votes

Yes, I do believe there is an issue here, and I've logged a bug here in our repo to track it.

To work around this until we fix it, you can bind to and use the DocumentClient directly to perform the update, e.g.:

public static async Task Run(
    string input, Binder binder, DocumentClient client, TraceWriter log)
{
    var docId = "c31d48aa-d74b-46a3-8ba6-0d4c6f288559";
    var document = await binder.BindAsync<JObject>(
                new DocumentDBAttribute("ItemDb", "ItemCollection")
    {
        ConnectionStringSetting = "<mydb>",
        Id = docId
    });

    log.Info("Item before: " +  document.ToString());
    document["text"] = "Modified!";

    var docUri = UriFactory.CreateDocumentUri("ItemDb", "ItemCollection", docId);
    await client.ReplaceDocumentAsync(docUri, document);
}

However, once you're using DocumentClient directly like this, it might turn out that you can just use it directly for all of your operations, your call. For example:

public static async Task Run(
    string input, DocumentClient client, TraceWriter log)
{
    var docId = "c31d48aa-d74b-46a3-8ba6-0d4c6f288559";
    var docUri = UriFactory.CreateDocumentUri("ItemDb", "ItemCollection", docId);

    var response = await client.ReadDocumentAsync(docUri);
    dynamic document = response.Resource;

    log.Info("Value: " +  dynamic.text);
    document.text = "Modified!"; 

    await client.ReplaceDocumentAsync(docUri, document);
}