Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions samples/AzureFunctionsApp/Entities/Counter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,40 @@ public static Task DispatchAsync([EntityTrigger] TaskEntityDispatcher dispatcher
}
}

public static class ManualCounter
{
[Function("Counter_Manual")]
public static Task DispatchAsync([EntityTrigger] TaskEntityDispatcher dispatcher)
{
return dispatcher.DispatchAsync(operation =>
{
if (operation.State.GetState(typeof(int)) is null)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shows how to do this before any of the new APIs. If we think this is good enough, can always revert the new API changes here.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the new APIs would make more sense here I think.

{
operation.State.SetState(0);
}

switch (operation.Name.ToLowerInvariant())
{
case "add":
int state = operation.State.GetState<int>();
state += operation.GetInput<int>();
operation.State.SetState(state);
return new(state);
case "reset":
operation.State.SetState(0);
break;
case "get":
return new((operation.State.GetState(typeof(int)) as int?) ?? 0);
case "delete":
operation.State.SetState(null);
break;
}

return default;
});
}
}

public static class CounterApis
{
/// <summary>
Expand All @@ -113,6 +147,10 @@ public static class CounterApis
/// Add to <see cref="Counter"/>, using the static method.
/// POST /api/counters/{id}/add/{value}?&mode=2
/// POST /api/counters/{id}/add/{value}?&mode=static
///
/// Add to <see cref="ManualCounter"/>.
/// POST /api/counters/{id}/add/{value}?&mode=3
/// POST /api/counters/{id}/add/{value}?&mode=manual
/// </summary>
[Function("Counter_Add")]
public static async Task<HttpResponseData> AddAsync(
Expand Down Expand Up @@ -242,6 +280,7 @@ static EntityInstanceId GetEntityId(HttpRequestData request, string key)
{
1 => "counter_state",
2 => "counter_alt",
3 => "counter_manual",
_ => "counter",
};
}
Expand All @@ -251,6 +290,7 @@ static EntityInstanceId GetEntityId(HttpRequestData request, string key)
{
"state" => "counter_state",
"static" => "counter_alt",
"manual" => "counter_manual",
_ => "counter",
};
}
Expand Down
1 change: 1 addition & 0 deletions samples/AzureFunctionsApp/Entities/counters.http
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// mode=0 or mode=entity (default) - dispatch to "counter" entity
// mode=1 or mode=state - dispatch to "counter_state" entity
// mode=2 or mode=static - dispatch to "counter_alt" entity
// mode=3 or mode=manual - dispatch to "counter_manual" entity
//
// "counter" and "counter_alt" are the same entities, however they use
// two different functions to dispatch, and thus are different entities when
Expand Down
21 changes: 20 additions & 1 deletion src/Abstractions/Entities/TaskEntityState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,32 @@ namespace Microsoft.DurableTask.Entities;
/// </summary>
public abstract class TaskEntityState
{
/// <summary>
/// Gets a value indicating whether this entity has state or not yet / anymore.
/// </summary>
public abstract bool HasState { get; }

/// <summary>
/// Gets the current state of the entity. This will return <c>null</c> if no state is present, regardless if
/// <typeparamref name="T"/> is a value-type or not.
/// </summary>
/// <typeparam name="T">The type to retrieve.</typeparam>
/// <param name="defaultValue">The default value to return if no state is present.</param>
/// <returns>The entity state.</returns>
public virtual T? GetState<T>() => (T?)this.GetState(typeof(T));
/// <remarks>
/// If no state is present, then <paramref see="defaultValue"/> will be returned but it will <b>not</b> be persisted
/// to <see cref="SetState"/>. This must be manually called.
/// </remarks>
public virtual T? GetState<T>(T? defaultValue = default)
{
object? state = this.GetState(typeof(T));
if (state is T typedState)
{
return typedState;
}

return defaultValue;
}

/// <summary>
/// Gets the current state of the entity. This will return <c>null</c> if no state is present, regardless if
Expand Down
14 changes: 8 additions & 6 deletions src/Worker/Core/Shims/TaskEntityShim.cs
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes here are to make it a bit more robust when it is accessed multiple times with different types. Today the following would throw:

operation.State.GetState(typeof(object));
operation.State.GetState(typeof(int));

With this change, it will be possible.

Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,16 @@ class StateShim : TaskEntityState

string? value;
object? cachedValue;
bool cacheValid;
string? checkpointValue;

public StateShim(DataConverter dataConverter)
{
this.dataConverter = dataConverter;
}

/// <inheritdoc />
public override bool HasState => this.value != null;

public string? CurrentState
{
get => this.value;
Expand All @@ -117,7 +119,6 @@ public string? CurrentState
{
this.value = value;
this.cachedValue = null;
this.cacheValid = false;
}
}
}
Expand All @@ -130,29 +131,30 @@ public void Commit()
public void Rollback()
{
this.CurrentState = this.checkpointValue;
this.cachedValue = null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that clearing the cache explicitly seems like a good idea here.

(since the code may have modified the object without calling SetState and then the comparison in the setter does not catch that it was modified).

}

public void Reset()
{
this.CurrentState = default;
this.cachedValue = null;
}

public override object? GetState(Type type)
{
if (!this.cacheValid)
if (this.cachedValue?.GetType() is Type t && t.IsAssignableFrom(type))
{
this.cachedValue = this.dataConverter.Deserialize(this.value, type);
this.cacheValid = true;
return this.cachedValue;
}

this.cachedValue = this.dataConverter.Deserialize(this.value, type);
return this.cachedValue;
}

public override void SetState(object? state)
{
this.value = this.dataConverter.Serialize(state);
this.cachedValue = state;
this.cacheValid = true;
}
}

Expand Down
2 changes: 2 additions & 0 deletions test/Abstractions.Tests/Entities/Mocks/TestEntityState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public TestEntityState(object? state)
this.State = state;
}

public override bool HasState => this.State != null;

public object? State { get; private set; }

public override object? GetState(Type type)
Expand Down