Skip to content
This repository has been archived by the owner on Oct 7, 2023. It is now read-only.

Commit

Permalink
fix(breaking): remove Upsert methods in favor of Replace
Browse files Browse the repository at this point in the history
* The Upsert methods will need to first be fixed to work with the "optimistic concurrency" pattern. When the ETag mismatches, it should actually not consider it as a new document and try to Insert, because that generates a DuplicateKey exception.
* Catch CosmosException with PreconditionFailed status and throw a DocumentETagMismatchException instead
  • Loading branch information
francoishill committed Mar 18, 2023
1 parent 27eaba2 commit 2eface9
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 26 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish-nuget-packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:

env:
DOTNET_VERSION: '7.0'
PACKAGE_VERSION: "0.20.2"
PACKAGE_VERSION: "0.21.0"

jobs:
build-and-deploy:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ Task<T> AddItemAsync(
T item,
CancellationToken cancellationToken = default);

Task<T> UpsertItemAsync(
Task<T> ReplaceItemAsync(
T item,
bool ignoreETag = false,
CancellationToken cancellationToken = default);

Task<T> UpsertItemAsync(
Task<T> ReplaceItemAsync(
T item,
CancellationToken cancellationToken = default);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Firepuma.DatabaseRepositories.Abstractions.Exceptions;
using System.Net;
using Firepuma.DatabaseRepositories.Abstractions.Exceptions;
using Firepuma.DatabaseRepositories.Abstractions.QuerySpecifications;
using Firepuma.DatabaseRepositories.Abstractions.Repositories;
using Firepuma.DatabaseRepositories.Abstractions.Repositories.Exceptions;
Expand Down Expand Up @@ -88,7 +89,7 @@ public async Task<int> GetItemsCountAsync(

return response.Resource;
}
catch (CosmosException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
return default;
}
Expand Down Expand Up @@ -155,14 +156,14 @@ public async Task<T> AddItemAsync(
return response.Resource;
}

public async Task<T> UpsertItemAsync(
public async Task<T> ReplaceItemAsync(
T item,
bool ignoreETag,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(item.Id))
{
throw new InvalidOperationException($"Item Id is required to be non-empty before calling CosmosDbRepository.UpsertItemAsync");
throw new InvalidOperationException($"Item Id is required to be non-empty before calling CosmosDbRepository.ReplaceItemAsync");
}

var options = new ItemRequestOptions();
Expand All @@ -173,23 +174,34 @@ public async Task<T> UpsertItemAsync(
}

Logger.LogDebug(
"Will now upsert item id {Id} in container {Container}",
"Will now replace item id {Id} in container {Container}",
item.Id, Container.Id);

var response = await Container.UpsertItemAsync<T>(item, ResolvePartitionKey(item.Id), options, cancellationToken);
try
{
var response = await Container.ReplaceItemAsync<T>(item, item.Id, ResolvePartitionKey(item.Id), options, cancellationToken);

Logger.LogInformation(
"Upserted item id {Id} in container {Container}, which consumed {Charge} RUs",
item.Id, Container.Id, response.RequestCharge);
Logger.LogInformation(
"Replace item id {Id} in container {Container}, which consumed {Charge} RUs",
item.Id, Container.Id, response.RequestCharge);

return response.Resource;
return response.Resource;
}
catch (CosmosException cosmosException) when (cosmosException.StatusCode == HttpStatusCode.PreconditionFailed)
{
Logger.LogWarning(
"Failed to replace item id {Id} in container {Container} due to PreconditionFailed response status but it consumed {Charge} RUs",
item.Id, Container.Id, cosmosException.RequestCharge);

throw new DocumentETagMismatchException();
}
}

public async Task<T> UpsertItemAsync(
public async Task<T> ReplaceItemAsync(
T item,
CancellationToken cancellationToken = default)
{
return await UpsertItemAsync(item, ignoreETag: false, cancellationToken);
return await ReplaceItemAsync(item, ignoreETag: false, cancellationToken);
}

public async Task DeleteItemAsync(
Expand All @@ -210,11 +222,22 @@ public async Task DeleteItemAsync(
"Will now delete item id {Id} from container {Container}",
item.Id, Container.Id);

var response = await Container.DeleteItemAsync<T>(item.Id, ResolvePartitionKey(item.Id), options, cancellationToken);
try
{
var response = await Container.DeleteItemAsync<T>(item.Id, ResolvePartitionKey(item.Id), options, cancellationToken);

Logger.LogInformation(
"Deleted item id {Id} from container {Container}, which consumed {Charge} RUs",
item.Id, Container.Id, response.RequestCharge);
Logger.LogInformation(
"Deleted item id {Id} from container {Container}, which consumed {Charge} RUs",
item.Id, Container.Id, response.RequestCharge);
}
catch (CosmosException cosmosException) when (cosmosException.StatusCode == HttpStatusCode.PreconditionFailed)
{
Logger.LogWarning(
"Failed to delete id {Id} from container {Container} due to PreconditionFailed response status but it consumed {Charge} RUs",
item.Id, Container.Id, cosmosException.RequestCharge);

throw new DocumentETagMismatchException();
}
}

public async Task DeleteItemAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,21 +131,21 @@ public async Task<T> AddItemAsync(T item, CancellationToken cancellationToken =
return item;
}

public async Task<T> UpsertItemAsync(
public async Task<T> ReplaceItemAsync(
T item,
bool ignoreETag = false,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(item.Id))
{
throw new InvalidOperationException($"Item Id is required to be non-empty before calling MongoDbRepository.UpsertItemAsync");
throw new InvalidOperationException($"Item Id is required to be non-empty before calling MongoDbRepository.ReplaceItemAsync");
}

var oldETag = item.ETag;

var options = new ReplaceOptions
{
IsUpsert = true,
IsUpsert = false,
};

Expression<Func<T, bool>> filter =
Expand All @@ -156,7 +156,7 @@ public async Task<T> UpsertItemAsync(
item.ETag = GenerateETag();

Logger.LogDebug(
"Will now upsert item id {Id} in collection {Collection}",
"Will now replace item id {Id} in collection {Collection}",
item.Id, CollectionNameForLogs);

var replaceResult = await Collection.ReplaceOneAsync(filter, item, options, cancellationToken);
Expand All @@ -172,17 +172,17 @@ public async Task<T> UpsertItemAsync(
}

Logger.LogInformation(
"Upserted item id {Id} in collection {Collection}",
"Replaced item id {Id} in collection {Collection}",
item.Id, CollectionNameForLogs);

return item;
}

public async Task<T> UpsertItemAsync(
public async Task<T> ReplaceItemAsync(
T item,
CancellationToken cancellationToken = default)
{
return await UpsertItemAsync(item, ignoreETag: false, cancellationToken);
return await ReplaceItemAsync(item, ignoreETag: false, cancellationToken);
}

protected async Task UpdateItemAsync(
Expand Down

0 comments on commit 2eface9

Please sign in to comment.