Skip to content

Commit 4a0ac42

Browse files
committed
align entity API defaults with .NET SDK and add TypedEntityMetadata<T>
1 parent 32a5de2 commit 4a0ac42

8 files changed

Lines changed: 300 additions & 15 deletions

File tree

client/src/main/java/com/microsoft/durabletask/DurableEntityClient.java

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,16 @@ public abstract void signalEntity(
8080
@Nullable SignalEntityOptions options);
8181

8282
/**
83-
* Fetches the metadata for a durable entity instance, excluding its state.
83+
* Fetches the metadata for a durable entity instance, including its state by default.
84+
* <p>
85+
* This matches the .NET SDK behavior where {@code includeState} defaults to {@code true}.
8486
*
8587
* @param entityId the entity instance ID to query
8688
* @return the entity metadata, or {@code null} if the entity does not exist
8789
*/
8890
@Nullable
8991
public EntityMetadata getEntityMetadata(EntityInstanceId entityId) {
90-
return this.getEntityMetadata(entityId, false);
92+
return this.getEntityMetadata(entityId, true);
9193
}
9294

9395
/**
@@ -100,6 +102,36 @@ public EntityMetadata getEntityMetadata(EntityInstanceId entityId) {
100102
@Nullable
101103
public abstract EntityMetadata getEntityMetadata(EntityInstanceId entityId, boolean includeState);
102104

105+
/**
106+
* Fetches the metadata for a durable entity instance with typed state access.
107+
* <p>
108+
* This always includes state in the result, matching the .NET SDK's
109+
* {@code GetEntityAsync<T>()} pattern. The returned {@link TypedEntityMetadata} provides
110+
* a {@link TypedEntityMetadata#getState()} method for direct typed state access.
111+
*
112+
* <pre>{@code
113+
* TypedEntityMetadata<Integer> metadata = client.getEntities()
114+
* .getEntityMetadata(entityId, Integer.class);
115+
* if (metadata != null) {
116+
* Integer state = metadata.getState();
117+
* System.out.println("Counter value: " + state);
118+
* }
119+
* }</pre>
120+
*
121+
* @param entityId the entity instance ID to query
122+
* @param stateType the class to deserialize the entity's state into
123+
* @param <T> the entity state type
124+
* @return the typed entity metadata with state, or {@code null} if the entity does not exist
125+
*/
126+
@Nullable
127+
public <T> TypedEntityMetadata<T> getEntityMetadata(EntityInstanceId entityId, Class<T> stateType) {
128+
EntityMetadata metadata = this.getEntityMetadata(entityId, true);
129+
if (metadata == null) {
130+
return null;
131+
}
132+
return new TypedEntityMetadata<>(metadata, stateType);
133+
}
134+
103135
/**
104136
* Queries the durable store for entity instances matching the specified filter criteria.
105137
*
@@ -149,6 +181,44 @@ public EntityQueryPageable getAllEntities() {
149181
return getAllEntities(new EntityQuery());
150182
}
151183

184+
/**
185+
* Returns an auto-paginating iterable over entity instances matching the specified filter criteria,
186+
* with state included for typed access.
187+
* <p>
188+
* This convenience overload ensures that entity state is fetched, matching the .NET SDK's
189+
* {@code GetAllEntitiesAsync<T>()} pattern. Use {@link EntityMetadata#readStateAs(Class)} on
190+
* each result to access the typed state.
191+
* <p>
192+
* Note: The provided query's {@code includeState} setting is preserved. A copy of the query
193+
* is made with {@code includeState} set to {@code true} so the original query is not modified.
194+
*
195+
* <pre>{@code
196+
* EntityQuery query = new EntityQuery().setInstanceIdStartsWith("counter");
197+
* for (EntityMetadata entity : client.getEntities().getAllEntities(query, Integer.class)) {
198+
* Integer state = entity.readStateAs(Integer.class);
199+
* System.out.println("Counter value: " + state);
200+
* }
201+
* }</pre>
202+
*
203+
* @param query the query filter criteria
204+
* @param stateType the expected type of the entity's state, used with
205+
* {@link EntityMetadata#readStateAs(Class)} for deserialization
206+
* @param <T> the entity state type
207+
* @return a pageable iterable over all matching entities with state included
208+
*/
209+
public <T> EntityQueryPageable getAllEntities(EntityQuery query, Class<T> stateType) {
210+
// Create a copy with includeState=true so we don't mutate the caller's query
211+
EntityQuery typedQuery = new EntityQuery()
212+
.setInstanceIdStartsWith(query.getInstanceIdStartsWith())
213+
.setLastModifiedFrom(query.getLastModifiedFrom())
214+
.setLastModifiedTo(query.getLastModifiedTo())
215+
.setIncludeState(true)
216+
.setIncludeTransient(query.isIncludeTransient())
217+
.setPageSize(query.getPageSize())
218+
.setContinuationToken(query.getContinuationToken());
219+
return new EntityQueryPageable(typedQuery, this::queryEntities);
220+
}
221+
152222
/**
153223
* Cleans up entity storage by removing empty entities and/or releasing orphaned locks.
154224
* <p>

client/src/main/java/com/microsoft/durabletask/EntityMetadata.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@
77

88
/**
99
* Represents metadata about a durable entity instance, including its identity, state, and lock status.
10+
* <p>
11+
* For typed state access, see {@link TypedEntityMetadata} which provides a {@code getState()} method
12+
* that returns the deserialized state as a specific type.
13+
*
14+
* @see TypedEntityMetadata
1015
*/
11-
public final class EntityMetadata {
16+
public class EntityMetadata {
1217
private final String instanceId;
1318
private final Instant lastModifiedTime;
1419
private final int backlogQueueSize;
@@ -115,6 +120,17 @@ public boolean isIncludesState() {
115120
return this.includesState;
116121
}
117122

123+
/**
124+
* Gets the data converter used for state deserialization.
125+
* <p>
126+
* This is package-private to allow {@link TypedEntityMetadata} to pass it to the superclass constructor.
127+
*
128+
* @return the data converter
129+
*/
130+
DataConverter getDataConverter() {
131+
return this.dataConverter;
132+
}
133+
118134
/**
119135
* Deserializes the entity state into an object of the specified type.
120136
*

client/src/main/java/com/microsoft/durabletask/EntityQuery.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,17 @@
1313
* {@link DurableEntityClient#queryEntities(EntityQuery)}.
1414
*/
1515
public final class EntityQuery {
16+
17+
/**
18+
* The default page size for entity queries ({@value}).
19+
* This matches the .NET SDK's {@code EntityQuery.DefaultPageSize}.
20+
*/
21+
public static final int DEFAULT_PAGE_SIZE = 100;
22+
1623
private String instanceIdStartsWith;
1724
private Instant lastModifiedFrom;
1825
private Instant lastModifiedTo;
19-
private boolean includeState;
26+
private boolean includeState = true;
2027
private boolean includeTransient;
2128
private Integer pageSize;
2229
private String continuationToken;

client/src/main/java/com/microsoft/durabletask/TaskEntity.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,13 @@ public abstract class TaskEntity<TState> implements ITaskEntity {
6565

6666
/**
6767
* Controls whether operations can be dispatched to methods on the state object.
68-
* When {@code true} (the default), if no matching method is found on the entity class itself,
68+
* When {@code true}, if no matching method is found on the entity class itself,
6969
* the framework will look for a matching method on the state object.
70-
* When {@code false}, only methods on the entity class are considered.
70+
* When {@code false} (the default), only methods on the entity class are considered.
71+
* <p>
72+
* This matches the .NET SDK default where {@code AllowStateDispatch} is {@code false}.
7173
*/
72-
private boolean allowStateDispatch = true;
74+
private boolean allowStateDispatch = false;
7375

7476
// Cache for resolved methods, keyed by (class, operationName).
7577
// Uses Optional<Method> so that "not found" results are also cached.
@@ -93,9 +95,9 @@ protected boolean getAllowStateDispatch() {
9395
/**
9496
* Sets whether operations can be dispatched to methods on the state object.
9597
* <p>
96-
* When {@code true} (the default), if no matching method is found on the entity class itself,
98+
* When {@code true}, if no matching method is found on the entity class itself,
9799
* the framework will look for a matching method on the state object.
98-
* When {@code false}, only methods on the entity class are considered.
100+
* When {@code false} (the default), only methods on the entity class are considered.
99101
*
100102
* @param allowStateDispatch {@code true} to allow state dispatch, {@code false} to disable
101103
*/
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
package com.microsoft.durabletask;
4+
5+
import javax.annotation.Nullable;
6+
7+
/**
8+
* An extension of {@link EntityMetadata} that provides typed access to the entity's state.
9+
* <p>
10+
* This mirrors the .NET SDK's {@code EntityMetadata<TState>} which inherits from {@code EntityMetadata}
11+
* and provides a typed {@code State} property. In Java, the state is eagerly deserialized and accessible
12+
* via {@link #getState()}.
13+
*
14+
* <h3>Example:</h3>
15+
* <pre>{@code
16+
* TypedEntityMetadata<Integer> metadata = client.getEntities()
17+
* .getEntityMetadata(entityId, Integer.class);
18+
* if (metadata != null) {
19+
* Integer state = metadata.getState();
20+
* System.out.println("Counter value: " + state);
21+
* }
22+
* }</pre>
23+
*
24+
* @param <T> the type of the entity's state
25+
* @see EntityMetadata
26+
* @see DurableEntityClient#getEntityMetadata(EntityInstanceId, Class)
27+
*/
28+
public final class TypedEntityMetadata<T> extends EntityMetadata {
29+
30+
private final T state;
31+
private final Class<T> stateType;
32+
33+
/**
34+
* Creates a new {@code TypedEntityMetadata} from an existing {@link EntityMetadata} and a state type.
35+
* <p>
36+
* The state is eagerly deserialized from the metadata's serialized state.
37+
*
38+
* @param source the source metadata to wrap
39+
* @param stateType the class to deserialize the state into
40+
*/
41+
TypedEntityMetadata(EntityMetadata source, Class<T> stateType) {
42+
super(
43+
source.getInstanceId(),
44+
source.getLastModifiedTime(),
45+
source.getBacklogQueueSize(),
46+
source.getLockedBy(),
47+
source.getSerializedState(),
48+
source.isIncludesState(),
49+
source.getDataConverter());
50+
this.stateType = stateType;
51+
this.state = source.readStateAs(stateType);
52+
}
53+
54+
/**
55+
* Gets the deserialized entity state.
56+
* <p>
57+
* Returns {@code null} if the entity has no state or if state was not included in the query.
58+
*
59+
* @return the deserialized state, or {@code null}
60+
*/
61+
@Nullable
62+
public T getState() {
63+
return this.state;
64+
}
65+
66+
/**
67+
* Gets the state type class used for deserialization.
68+
*
69+
* @return the state type class
70+
*/
71+
public Class<T> getStateType() {
72+
return this.stateType;
73+
}
74+
}

client/src/test/java/com/microsoft/durabletask/EntityQueryTest.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,28 @@ void setInstanceIdStartsWith_mixedCase_lowercased() {
5656
// region defaults
5757

5858
@Test
59-
void defaults_allNullOrFalse() {
59+
void defaults_includeStateTrue_othersNullOrFalse() {
6060
EntityQuery query = new EntityQuery();
6161
assertNull(query.getInstanceIdStartsWith());
6262
assertNull(query.getLastModifiedFrom());
6363
assertNull(query.getLastModifiedTo());
64-
assertFalse(query.isIncludeState());
64+
assertTrue(query.isIncludeState());
6565
assertFalse(query.isIncludeTransient());
6666
assertNull(query.getPageSize());
6767
assertNull(query.getContinuationToken());
6868
}
6969

7070
// endregion
7171

72+
// region DefaultPageSize constant
73+
74+
@Test
75+
void defaultPageSize_is100() {
76+
assertEquals(100, EntityQuery.DEFAULT_PAGE_SIZE);
77+
}
78+
79+
// endregion
80+
7281
// region setter/getter round-trip tests
7382

7483
@Test

client/src/test/java/com/microsoft/durabletask/TaskEntityTest.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,15 @@ public void setValue(int value) {
105105

106106
/**
107107
* Entity that has no matching method but whose state type has the method (state dispatch).
108+
* Explicitly enables state dispatch since the default is now {@code false}.
108109
*/
109110
static class StateDispatchEntity extends TaskEntity<MyState> {
110111
// No "increment" method on the entity itself — should dispatch to MyState.increment()
111112

113+
public StateDispatchEntity() {
114+
setAllowStateDispatch(true);
115+
}
116+
112117
@Override
113118
protected Class<MyState> getStateType() {
114119
return MyState.class;
@@ -336,10 +341,10 @@ void stateDispatch_disabledWithAllowStateDispatchFalse() {
336341
}
337342

338343
@Test
339-
void stateDispatch_enabledByDefault() throws Exception {
340-
// StateDispatchEntity has allowStateDispatch=true (default)
341-
StateDispatchEntity entity = new StateDispatchEntity();
342-
assertTrue(entity.getAllowStateDispatch());
344+
void stateDispatch_disabledByDefault() throws Exception {
345+
// Default is now false, matching the .NET SDK
346+
CounterEntity entity = new CounterEntity();
347+
assertFalse(entity.getAllowStateDispatch());
343348
}
344349

345350
// endregion

0 commit comments

Comments
 (0)