I have implemented an idempotent mechanism to stop users submitting duplicated create requests (e.g. users with slow international connections). Before I role it out to production, I would like to ask whether there is a more recommended approach with ServiceStack.
I require a Guid idempotent token on each form instance for creating entities, which is then checked against a record of previous tokens to detect duplicated request from the form instance.
// Abstract base for all create requests to enforce token to detect duplicated requests.
public abstract class CreateRequest
{
public Guid IdempotentToken { get; init; }
}
// Create request for UserSetting entity
public class CreateUserSetting : CreateRequest, ICreateDb<UserSetting>, IReturn<UserSetting>, IPost
{
public Guid AccountId { get; init; }
public string KeyName { get; init; } = null!;
public string KeyValue { get; init; } = null!;
}
I define a global request and response filter to handle the duplication checks on a create request
private void ConfigureGlobalRequestFilters()
{
GlobalRequestFilters.Add((req, res, dto) =>
{
// reject duplicated create requests
if (req.Dto is IPost and CreateRequest createRequest)
{
if (Guid.Empty.Equals(createRequest.IdempotentToken))
{
throw new ArgumentException ("Form's Idempotent token missing");
}
using var db = Resolve<IDbConnectionFactory>().Open();
var found = db.Single<Idempotent>(x => x.Token == createRequest.IdempotentToken);
if (found is not null)
{
throw new OptimisticConcurrencyException("Duplicated create request submitted");
}
}
});
}
private void ConfigureGlobalResponseFilters()
{
GlobalResponseFilters.Add((req, res, dto) =>
{
if (req.Dto is IPost and CreateRequest createRequest)
{
// cache request's token if not error response
if (!res.Dto.IsErrorResponse())
{
using var db = Resolve<IDbConnectionFactory>().Open();
// cache new token for a successful create request
db.Insert(new Idempotent
{
Token = createRequest.IdempotentToken,
RequestTstp = DateTime.UtcNow
});
}
}
});
}
The Idempotent is a simple MSSQL table. I clear the previous cached tokens that are more than 60 minutes old each time AppHost starts.
[Schema("Log")]
[DataContract]
public record Idempotent
{
[DataMember, Required]
public Guid Token { get; set; }
[DataMember, Required]
public DateTime RequestTstp { get; set; }
}
I only use the above approach for creates, because I depend on the RowVersion property for duplicated update requests.
So, before I role it out to production, is there is a better approach with ServiceStack?