I am implementing a DynamoDBContext decorator that will perform sensitive data tokenization on objects when a value is written or loaded. The class must implement IDynamoDBContext so that it can be injected into its dependents. The implementation uses a third party tokenizer. The tokenization interface has a generic constraint to only tokenize reference "class" types.
The interfaces are both from third party libraries. I cannot change the interface signatures of either.
interface IDynamoDBContext {
Task SaveAsync<T>(T value, CancellationToken cancellationToken = default);
// ...
}
interface ITokenizer {
Task<T> TokenizeAsync<T>(T value, CancellationToken cancellationToken = default) where T : class;
// ...
}
class TokenizingDynamoDBContext : IDynamoDBContext {
private readonly IDynamoDBContext _baseContext;
private readonly ITokenizer _tokenizer;
// ...
public async Task SaveAsync<T>(T value, CancellationToken cancellationToken = default) {
if (typeof(T).IsClass)
value = await _tokenizer.TokenizeAsync(value, cancellationToken);
await _baseContext.SaveAsync(value, cancellationToken);
}
// ...
}
As presented using the runtime checks to ensure the constraints produces a compiler error:
CS0452 The type 'T' must be a reference type in order to use it as parameter 'T' in the generic type or method 'ITokenizer.TokenizeAsync(T)'
I cannot simply cast the TokenizeAsync parameter as object
. The tokenizer uses type info from the generic type parameter. Casting the value as object
will result in the value properties not being tokenized correctly.
Attempting to add the where T : class
constraint to the TokenizingDynamoDBContext
method causes a compiler error:
CS0425 The constraints for type parameter 'T' of method 'TokenizingDynamoDBContext.DeleteAsync(T, CancellationToken)' must match the constraints for type parameter 'T' of interface method 'IDynamoDBContext.DeleteAsync(T, CancellationToken)'. Consider using an explicit interface implementation instead. Dynamo.DataModel
Using explicit interface implementation means that the tokenizing code won't be reached when this is used by its dependents.
I've also tried this:
class TokenizingDynamoDBContext {
// ...
public async Task SaveAsync<T>(T value, CancellationToken cancellationToken = default) {
await SaveAsyncImpl(value, cancellationToken);
}
private async Task SaveAsyncImpl<T>(T value, CancellationToken cancellationToken) where T : class {
value = await _tokenizer.TokenizeAsync(value, cancellationToken);
await _baseContext.SaveAsync(value, cancellationToken);
}
private async Task SaveAsyncImpl<T>(T value, CancellationToken cancellationToken) where T : struct {
await _baseContext.SaveAsync(value, cancellationToken);
}
}
This produces a compiler error as well with or without the second where T : struct
constraint:
CS0111 Type 'TokenizingDynamoDBContext' already defines a member called 'SaveAsyncImpl' with the same parameter types
What is the proper way to make this work?
In C# generic constraints are not a part of method signature so the last try will not work. As for the issue itself - currently compiler does not support something like pattern matching on the generic type parameter, so the only workaround (at least I know of) is to use some reflection.
public async Task SaveAsync<T>(T value, CancellationToken cancellationToken = default) {
if (typeof(T).IsClass)
{
var method = typeof(ITokenizer)
.GetMethod(nameof(ITokenizer.TokenizeAsync))
.MakeGenericMethod(typeof(T));
value = await (method.Invoke(_tokenizer, new object[] { value, cancellationToken }) as Task<T>);
}
await _baseContext.SaveAsync(value, cancellationToken);
}
If the part of the code is performance critical you can reduce the reflection cost by "caching" it (see for example this answer).
See also: