Azure-functions-durable-extension: Clarify usage patterns of DispatchAsync()

Created on 15 Nov 2020  Â·  9Comments  Â·  Source: Azure/azure-functions-durable-extension

I'm trying to set the initial state of an entity using ctx.SetState

The problem is that I'm using DI and output bindings, so I've had to make a parameterless constructor, in order to initialise the properties on the Entity in the ctx.SetState method.

However, since adding the parameterless ctor, my code is throwing null reference exceptions when I try to use the output bindings.

Am I doing something wrong here or is there a different/better way to set the state? cheers!

private readonly IAsyncCollector<SignalRMessage> _signalR;
private readonly ICollector<Microsoft.Azure.ServiceBus.Message> _queue;
private readonly UtilityA _utilityA;
private readonly UtilityB _utlityB;

[JsonProperty("propertyA")]
public string PropertyA { get; set; }

[JsonProperty("propertyB")]
public string PropertyB { get; set; }

[JsonProperty("propertyC")]
public string PropertyC { get; set; }

public FooEntity()
{
}

public FooEntity(
    UtilityA utilityA,
    UtilityB utilityB,
    IAsyncCollector<SignalRMessage> signalR,
    ICollector<Microsoft.Azure.ServiceBus.Message> queue)
{
    _utilityA = utilityA;
    _utilityB = utilityB;
    _signalR = signalR;
    _queue = queue;
}

[FunctionName(nameof(FooEntity))]
public static Task Run(
    [EntityTrigger] IDurableEntityContext ctx,
    [SignalR(...)] signalR,
    [ServiceBus(... )] queue)
{
    if (!ctx.HasState)
    {
        Entity.Current.SignalEntity<IFooEntity>(Entity.Current.EntityId, /*futuredate*/, proxy => proxy.InvokeBar());

        ctx.SetState(new FooEntity()
        {
            PropertyA = "some data",
            PropertyB = "some data",
            PropertyC = "some data"
        });
    }

    return ctx.DispatchAsync<IFooEntity>(signalR, queue);
}
analyzer documentation

Most helpful comment

@olitomlinson,

Sincerely sorry for the delay here. Other things came up and this slipped off my plate. Will get back to you today with an update.

All 9 comments

Hmm, this may be an edge case we did not consider. I will take a look and get back to you to see if this is a bug and if there are any workarounds.

Thanks @ConnorMcMahon

I thought I might be able to just pass an anonymous object/JObject which would then serialize/deserialize correctly to the Entity State properties, but that didn't work. It looks like there is some explicit type checking going on so the object passed in must be the Entity class itself.

@ConnorMcMahon hey did you manage to see if there were any work arounds?

This is starting to block me on a few use-cases where I'm trying to set the initial state of the entity and also trying to pass through a binding into the entities constructor from the dispatch method.

The binding appears to be null regardless of the binding type - input/output or otherwise.

Would it be possible allow SetState to just take a JObject? Then I wouldn't have to add a parameterless ctor of the Entity.

@olitomlinson,

Sincerely sorry for the delay here. Other things came up and this slipped off my plate. Will get back to you today with an update.

The crux of the issue lies with this code:

if (this.CurrentStateAccess == StateAccess.Accessed)
{
    return (TState)this.CurrentState;
}

Essentially, if you call any operations on IDurableEntityContext that edit state, you bypass the fancy reflection magic logic used by DispatchAsync(). The DispatchAsync() API was not designed with performing entity actions before dispatching in mind, so I would highly recommend against any business logic happening before you call DispatchAsync(). That static function should include exclusively boilerplate. In fact, it may be worth adding this case to our analyzer to highlight cases that could result in strange behavior.

As for workarounds/best practices, I have not been able to test these myself since my dev box is currently out of commission, but I have a somewhat high degree of confidence that this should work.

Approach 1 (Recommended): Feed in your IDurableEntityContext as a "binding" itself

private readonly IAsyncCollector<SignalRMessage> _signalR;
private readonly ICollector<Microsoft.Azure.ServiceBus.Message> _queue;
private readonly UtilityA _utilityA;
private readonly UtilityB _utlityB;

[JsonProperty("propertyA")]
public string PropertyA { get; set; }

[JsonProperty("propertyB")]
public string PropertyB { get; set; }

[JsonProperty("propertyC")]
public string PropertyC { get; set; }

public FooEntity(
    UtilityA utilityA,
    UtilityB utilityB,
    IDurableEntityContext ctx,
    IAsyncCollector<SignalRMessage> signalR,
    ICollector<Microsoft.Azure.ServiceBus.Message> queue)
{
    if (!ctx.HasState)
    {
        Entity.Current.SignalEntity<IFooEntity>(Entity.Current.EntityId, /*futuredate*/, proxy => proxy.InvokeBar());
        PropertyA = "some data";
        PropertyB = "some data";
        PropertyC = "some data";
    }
    _utilityA = utilityA;
    _utilityB = utilityB;
    _signalR = signalR;
    _queue = queue;
}

[FunctionName(nameof(FooEntity))]
public static Task Run(
    [EntityTrigger] IDurableEntityContext ctx,
    [SignalR(...)] signalR,
    [ServiceBus(... )] queue)
{
    return ctx.DispatchAsync<IFooEntity>(ctx, signalR, queue);
}

Approach 2 (Backup): Use some custom logic to default state

private readonly IAsyncCollector<SignalRMessage> _signalR;
private readonly ICollector<Microsoft.Azure.ServiceBus.Message> _queue;
private readonly UtilityA _utilityA;
private readonly UtilityB _utlityB;

[JsonProperty("propertyA")]
public string PropertyA { get; set; }

[JsonProperty("propertyB")]
public string PropertyB { get; set; }

[JsonProperty("propertyC")]
public string PropertyC { get; set; }

[JsonProperty("isInitialized")]
public bool IsInitialized { get; set; }

public FooEntity(
    UtilityA utilityA,
    UtilityB utilityB,
    IAsyncCollector<SignalRMessage> signalR,
    ICollector<Microsoft.Azure.ServiceBus.Message> queue)
{
    if (!IsInitialized)
    {
        Entity.Current.SignalEntity<IFooEntity>(Entity.Current.EntityId, /*futuredate*/, proxy => proxy.InvokeBar());
        PropertyA = "some data";
        PropertyB = "some data";
        PropertyC = "some data";
        IsInitialized = true;
    }
    _utilityA = utilityA;
    _utilityB = utilityB;
    _signalR = signalR;
    _queue = queue;
}

[FunctionName(nameof(FooEntity))]
public static Task Run(
    [EntityTrigger] IDurableEntityContext ctx,
    [SignalR(...)] signalR,
    [ServiceBus(... )] queue)
{
    return ctx.DispatchAsync<IFooEntity>(signalR, queue);
}

@ConnorMcMahon thanks for taking the time to look into this! I’ll give it a go in the morning and get back to you.

You mentioned not to use the entity for business logic before dispatch is called.

Would you say the usage of ‘entity.current.signal’ to signal itself at a future date, in both your examples, is legal and guaranteed?

Would you say the usage of ‘entity.current.signal’ to signal itself at a future date, in both your examples, is legal and guaranteed?

To clarify, my commentary about business logic before the dispatch is called would probably better be better stated as "I would not perform any business logic on the entity in your function calling DispatchAsync(). That function is meant pretty much to be exclusively boiler plate to invoke your class based entity. The business logic should happen inside of the class we construct.

To my knowledge, there is nothing wrong with calling Entity.Current.SignalEntity() to signal itself at a future time, be that in the first execution of the entity or any subsequent operation. I just moved it to the constructor in my examples to keep the business logic out of the boilerplate function, even though I don't believe it would technically mess with DispatchAsync().

@ConnorMcMahon

Thanks for the clarification!

Your first example seems to be working just fine from my tests! Thanks so much!

Might I recommend the public docs are updated to reflect this use-case?

Glad it worked @olitomlinson!

At the very least, we should definitely improve our documentation around DispatchAsync() to highlight the points made in our conversation above. I can also show in our docs sample that when you pass in bindings, passing in the IDurableEntityContext is also completely valid. I may even use the "initialize entity" for that, as I believe that is one of the more common usecases.

Was this page helpful?
0 / 5 - 0 ratings