Search code examples
c#azureazure-durable-functionsazure-http-trigger

Durable Entities - Counter Example


Total Azure Functions newbie here, but I feel like I have spent days researching this on my own and I'm just missing something. I am working on creating a simple counter entity that can be used for generating order tracking numbers:

Entry point:

public static class Counter
    {
        [FunctionName("GetTrackingNumber")]
        public static async Task<IActionResult> Get(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = "GetTrackingNumber")] HttpRequest req,
            [DurableClient] IDurableEntityClient client,
            ILogger log
            )
        {
            var entityId = new EntityId(nameof(CounterEntity), "myCounter");

            await client.SignalEntityAsync<ICounterEntity>(entityId, proxy => proxy.Add(1));

            var stateResponse = await client.ReadEntityStateAsync<CounterEntity>(entityId);
            string trackingNumber = "";

            if(stateResponse.EntityExists)
            {
                trackingNumber = await stateResponse.EntityState.GetNextTrackingNumber();
            }

            return new OkObjectResult(trackingNumber);            
        }
    }

Counter entity:

public interface ICounterEntity
    {
        [Deterministic]
        public void Add(int amount);

        [Deterministic]
        public Task<string> GetNextTrackingNumber();

        [Deterministic]
        public Task Reset();
    }

    [JsonObject(MemberSerialization.OptIn)]
    public class CounterEntity : ICounterEntity
    {

        private readonly Random _random = new Random();

        [JsonProperty("value")]
        public int Value { get; set; }

        [JsonProperty("prefix")]
        public string Prefix { get; set; }

        [JsonProperty("prefixList")]
        public List<String> PrefixList { get; set; }

        public CounterEntity()
        {
            PrefixList = new List<string>();
            Prefix = RandomString(3);
            PrefixList.Add(Prefix);
        }

        public void Add(int amount)
        {
            Value += amount;
        }

        public Task<string> GetNextTrackingNumber()
        {
            var thisTrackingNumber = String.Concat(Prefix, "-", string.Format("{0:00000000}", Value));
            return Task.FromResult(thisTrackingNumber);
        }

        public Task Reset()
        {
            Value = 0;
            Prefix = RandomString(3);
            PrefixList.Add(Prefix);
            return Task.CompletedTask;
        }
   
        public string RandomString(int size, bool lowerCase = false)
        {
            var builder = new StringBuilder(size);
            for (var i = 0; i < size; i++)
            {
                var @char = (char)_random.Next(offset, offset + lettersOffset);
                builder.Append(@char);
            }

            return lowerCase ? builder.ToString().ToLower() : builder.ToString();
        }

        [FunctionName(nameof(CounterEntity))]
        public static Task Run([EntityTrigger] IDurableEntityContext ctx) => ctx.DispatchAsync<CounterEntity>();


    }

I published the function to Azure and it looks like it works (sort of), but I'm not at all confident that it's right yet. The first time I called it, I got a blank response. Something similar happened the first couple of times I ran it the next day -- the function app probably had to spin up, which is fine, but the first two or three responses I received from sending the request came back showing the last number output from last night and then it started to increment as expected.

Can anyone with a little bit more experience in durable entities look over this and suggest what I may be doing wrong? Nearly a week of searching on this has yielded almost nothing useful.

Thanks for your help!!!


Solution

  • It is reasonable that you may get an empty response. The issue here is that you merely signal the entity but then there is no guarantee that by the time you check the entity state using function ReadEntityStateAsync it has been created successfully (it might still be in memory).

    ReadEntityStateAsync function only returns a snapshot of the entity state from some earlier point in time but such state might be stale. The Developer's guide to durable entities in .NET page has a note on this:

    The object returned by ReadEntityStateAsync is just a local copy, that is, a snapshot of the entity state from some earlier point in time. In particular, it may be stale, and modifying this object has no effect on the actual entity.

    Only orchestrations can read the state of an entity accurately. A crude workaround I employed before (to do all this in one HTTP call) is starting an orchestration (using StartNewAsync function of IDurableOrchestrationClient) and then calling your entity within that orchestration (rather than signalling it). You will wait in that orchestration to ensure the entity has been created.

    StartNewAsync will return an instanceId, so you can use WaitForCompletionOrCreateCheckStatusResponseAsync function of the IDurableOrchestrationClient in an attempt to wait for your orchestration to complete. It is only then that you should read your entity state using ReadEntityStateAsync

    The Durable Functions extension exposes built-in HTTP APIs. It also provides APIs for interacting with orchestrations and entities from within HTTP-triggered functions. You could certainly come up with a better solution if you don't mind making multiple HTTP calls at your client end.