in Microsoft.Azure.Cosmos.Samples/Usage/TransactionalBatch/Program.cs [98:341]
private static async Task RunDemoAsync()
{
Container gamesContainer = Program.database.GetContainer(containerId);
// This code demonstrates interactions by a multi-player game service that hosts games with the database to save game state.
// In this fictional game, players move about the 10x10 map and try to find balls.
// The objective is to collect 2 balls of the same color, or a golden ball if one appears.
// After 5 minutes, if the game is not complete, the player with highest number of balls wins.
int gameId = 420;
int playerCount = 3;
int ballCount = 4;
List<GameBall> balls = new List<GameBall>();
List<GameParticipant> players = new List<GameParticipant>();
Console.WriteLine("At the start of the game, the balls are added on the map, and the players are added ...");
// The below batch request is used to create the game balls and participants in an atomic fashion.
TransactionalBatchResponse gameStartResponse = await gamesContainer.CreateTransactionalBatch(new PartitionKey(gameId))
.CreateItem<GameBall>(GameBall.Create(gameId, Color.Red, 4, 2))
.CreateItem<GameBall>(GameBall.Create(gameId, Color.Blue, 6, 4))
.CreateItem<GameBall>(GameBall.Create(gameId, Color.Blue, 8, 7))
.CreateItem<GameBall>(GameBall.Create(gameId, Color.Red, 8, 8))
.CreateItem<GameParticipant>(GameParticipant.Create(gameId, "alice"))
.CreateItem<GameParticipant>(GameParticipant.Create(gameId, "bob"))
.CreateItem<GameParticipant>(GameParticipant.Create(gameId, "carla"))
.ExecuteAsync();
GameParticipant alice, bob, carla;
GameBall firstBlueBall, secondRedBall;
using (gameStartResponse)
{
// Batch requests do not throw exceptions on execution failures as long as the request is valid, so we need to check the response status explicitly.
// A HTTP 200 (OK) StatusCode on the batch response indicates that all operations succeeded.
// An example later demonstrates a failure case.
if (!gameStartResponse.IsSuccessStatusCode)
{
// Log and handle failure
LogFailure(gameStartResponse);
return;
}
// Refresh in-memory state from response.
// The TransactionalBatchResponse has a list of TransactionalBatchOperationResult, one for each operation within the batch request in the order
// the operations were added to the TransactionalBatch.
for (int index = 0; index < ballCount; index++)
{
// The GetOperationResultAtIndex method returns the result of the operation at the given index with a Resource deserialized to the provided type.
TransactionalBatchOperationResult<GameBall> gameBallResult = gameStartResponse.GetOperationResultAtIndex<GameBall>(index);
balls.Add(gameBallResult.Resource);
}
firstBlueBall = balls[1];
secondRedBall = balls[3];
for (int index = ballCount; index < gameStartResponse.Count; index++)
{
players.Add(gameStartResponse.GetOperationResultAtIndex<GameParticipant>(index).Resource);
}
alice = players.Single(p => p.Nickname == "alice");
bob = players.Single(p => p.Nickname == "bob");
carla = players.Single(p => p.Nickname == "carla");
}
PrintState(players, balls);
Console.WriteLine("Alice goes to 6, 4 and finds a blue ball ...");
alice.BlueCount++;
// Upserts maybe used to replace items or create them if they are not already present.
// An existing item maybe replaced along with concurrency checks the ETag returned in the responses of earlier requests on the item
// or without these checks if they are not required.
// Item deletes may also be a part of batch requests.
TransactionalBatchResponse aliceFoundBallResponse = await gamesContainer.CreateTransactionalBatch(new PartitionKey(gameId))
.UpsertItem<ParticipantLastActive>(ParticipantLastActive.Create(gameId, "alice"))
.ReplaceItem<GameParticipant>(alice.Nickname, alice, new TransactionalBatchItemRequestOptions { IfMatchEtag = alice.ETag })
.DeleteItem(firstBlueBall.Id)
.ExecuteAsync();
using (aliceFoundBallResponse)
{
if (!aliceFoundBallResponse.IsSuccessStatusCode)
{
// Log and handle failure
alice.BlueCount--;
LogFailure(aliceFoundBallResponse);
return;
}
// Refresh in-memory state from response.
balls.Remove(firstBlueBall);
// We only update the etag as we have the rest of the state we care about here already as needed.
alice.ETag = aliceFoundBallResponse[1].ETag;
}
PrintState(players, balls);
Console.WriteLine("Bob goes to 8, 8 and finds a red ball ...");
bob.RedCount++;
// Stream variants for all batch operations that accept an item are also available for use when the item is available as a Stream.
Stream bobIsActiveStream = ParticipantLastActive.CreateStream(gameId, "bob");
Stream bobAsStream = Program.AsStream(bob);
using (bobIsActiveStream)
using (bobAsStream)
{
TransactionalBatchResponse bobFoundBallResponse = await gamesContainer.CreateTransactionalBatch(new PartitionKey(gameId))
.UpsertItemStream(bobIsActiveStream)
.ReplaceItemStream(bob.Nickname, bobAsStream, new TransactionalBatchItemRequestOptions { IfMatchEtag = bob.ETag })
.DeleteItem(secondRedBall.Id)
.ExecuteAsync();
using (bobFoundBallResponse)
{
if (!bobFoundBallResponse.IsSuccessStatusCode)
{
// Log and handle failure.
bob.RedCount--;
LogFailure(bobFoundBallResponse);
return;
}
// Refresh in-memory state from response.
balls.Remove(secondRedBall);
// The resultant item for each operation is also available as a Stream that can be used for example if the response is just
// going to be transferred to some other system.
Stream updatedPlayerAsStream = bobFoundBallResponse[1].ResourceStream;
bob = Program.FromStream<GameParticipant>(updatedPlayerAsStream);
}
}
PrintState(players, balls);
Console.WriteLine("A golden ball appears near each of the players to select an instant winner ...");
TransactionalBatchResponse goldenBallResponse = await gamesContainer.CreateTransactionalBatch(new PartitionKey(gameId))
.CreateItem<GameBall>(GameBall.Create(gameId, Color.Gold, 2, 2))
.CreateItem<GameBall>(GameBall.Create(gameId, Color.Gold, 6, 3))
// oops - there is already a ball at 8, 7
.CreateItem<GameBall>(GameBall.Create(gameId, Color.Gold, 8, 7))
.ExecuteAsync();
using (goldenBallResponse)
{
// If an operation within the TransactionalBatch fails during execution, the TransactionalBatchResponse will have a status code of the failing operation.
// The TransactionalBatchOperationResult entries within the response can be read to get details about the specific operation that failed.
// The failing operation (for example if we have a conflict because we are trying to create an item
// that already exists) will have the StatusCode on its corresponding TransactionalBatchOperationResult set to the actual failure status
// (HttpStatusCode.Conflict in this example). All other result entries will have a status code of HTTP 424 Failed Dependency.
// In case any operation within a TransactionalBatch fails, no changes from the batch will be committed.
// Other status codes such as HTTP 429 (Too Many Requests) and HTTP 5xx on server errors may also be returned on the TransactionalBatchResponse.
if (!goldenBallResponse.IsSuccessStatusCode)
{
if (goldenBallResponse.StatusCode == HttpStatusCode.Conflict)
{
for (int index = 0; index < goldenBallResponse.Count; index++)
{
TransactionalBatchOperationResult operationResult = goldenBallResponse[index];
if ((int)operationResult.StatusCode == 424)
{
// This operation failed because it was in a TransactionalBatch along with another operation where the latter was the actual cause of failure.
continue;
}
else if (operationResult.StatusCode == HttpStatusCode.Conflict)
{
Console.WriteLine("Creation of the {0}rd golden ball failed because there was already an existing ball at that position.", index + 1);
}
}
}
else
{
// Log and handle other failures
LogFailure(goldenBallResponse);
return;
}
}
}
PrintState(players, balls);
Console.WriteLine("We need to end the game now; determining the winner as the player with highest balls ...");
// Batch requests may also be used to atomically read multiple items with the same partition key.
TransactionalBatchResponse playersResponse = await gamesContainer.CreateTransactionalBatch(new PartitionKey(gameId))
.ReadItem(alice.Nickname)
.ReadItem(bob.Nickname)
.ReadItem(carla.Nickname)
.ExecuteAsync();
GameParticipant winner = null;
bool isTied = false;
using (playersResponse)
{
if (!playersResponse.IsSuccessStatusCode)
{
// Log and handle failure
LogFailure(playersResponse);
return;
}
for (int index = 0; index < playerCount; index++)
{
GameParticipant current;
if (index == 0)
{
// The item returned can be made available as the required POCO type using GetOperationResultAtIndex.
// A single batch request can be used to read items that can be deserialized to different POCOs as well.
current = playersResponse.GetOperationResultAtIndex<GameParticipant>(index).Resource;
}
else
{
// The item returned can also instead be accessed directly as a Stream (for example to pass as-is to another component).
Stream aliceInfo = playersResponse[index].ResourceStream;
current = Program.FromStream<GameParticipant>(aliceInfo);
}
if (winner == null || current.TotalCount > winner.TotalCount)
{
winner = current;
isTied = false;
}
else if(current.TotalCount == winner.TotalCount)
{
isTied = true;
}
}
}
if (!isTied)
{
Console.WriteLine($"{winner.Nickname} has won the game!\n");
}
else
{
Console.WriteLine("The game is a tie; there is no clear winner.\n");
}
}