Skip to content

Commit 1f4bd3a

Browse files
committed
improved support for intermediary converters
1 parent 8b89e1c commit 1f4bd3a

File tree

5 files changed

+59
-35
lines changed

5 files changed

+59
-35
lines changed

src/Klean.EntityFrameworkCore.DataProtection/Extensions/ModelBuilderExt.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Microsoft.EntityFrameworkCore;
44
using Microsoft.EntityFrameworkCore.Metadata;
55
using Microsoft.EntityFrameworkCore.Metadata.Builders;
6+
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
67

78
namespace EntityFrameworkCore.DataProtection.Extensions;
89

@@ -62,12 +63,24 @@ where status.SupportsEncryption
6263
var propertyType = property.PropertyInfo?.PropertyType;
6364
var internalConverter = property.GetValueConverter();
6465

65-
if (propertyType == typeof(string))
66-
property.SetValueConverter(new StringDataProtectionValueConverter(protector, internalConverter));
67-
else if (propertyType == typeof(byte[]))
68-
property.SetValueConverter(new ByteArrayDataProtectionValueConverter(protector, internalConverter));
69-
else
70-
throw PropertyBuilderExt.InvalidTypeException;
66+
var internalConverterConvertTo = internalConverter?.GetType().BaseType?.GetGenericArguments()[1];
67+
68+
if (propertyType == typeof(string) || internalConverterConvertTo == typeof(string))
69+
{
70+
var converter = typeof(StringDataProtectionValueConverter<>).MakeGenericType(propertyType!);
71+
var instance = Activator.CreateInstance(converter, protector, internalConverter);
72+
property.SetValueConverter(instance as ValueConverter);
73+
}
74+
else if (propertyType == typeof(byte[]) || internalConverterConvertTo == typeof(byte[]))
75+
{
76+
var converter = typeof(ByteArrayDataProtectionValueConverter<>).MakeGenericType(propertyType!);
77+
var instance = Activator.CreateInstance(converter, protector, internalConverter);
78+
property.SetValueConverter(instance as ValueConverter);
79+
}
80+
else if (internalConverter is null)
81+
throw new InvalidOperationException(
82+
"Only string and byte[] properties are supported for now. Alternatively use an intermediary converter (see esoteric readme section)" +
83+
"Please open an issue on https://github.com/ddjerqq/Klean.EntityFrameworkCore.DataProtection/issues to request a new feature");
7184

7285
if (status.SupportsQuerying)
7386
AddShadowProperty(entityType, property, status.IsUniqueIndex);

src/Klean.EntityFrameworkCore.DataProtection/Extensions/PropertyBuilderExt.cs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,6 @@ public static class PropertyBuilderExt
1313
private const string IsQueryableAnnotationName = "Klean.EntityFrameworkCore.DataProtection.IsQueryable";
1414
private const string IsUniqueIndexAnnotationName = "Klean.EntityFrameworkCore.DataProtection.IsUniqueIndex";
1515

16-
internal static readonly InvalidOperationException InvalidTypeException =
17-
new("Only string and byte[] properties are supported for now. " +
18-
"Please open an issue on https://github.com/ddjerqq/Klean.EntityFrameworkCore.DataProtection/issues to request a new feature");
19-
2016
/// <summary>
2117
/// Marks the property as encrypted.
2218
/// This will store the property as an encrypted string in the database.
@@ -28,9 +24,6 @@ public static class PropertyBuilderExt
2824
/// <returns>The same builder instance so that multiple configuration calls can be chained.</returns>
2925
public static PropertyBuilder<TProperty> IsEncrypted<TProperty>(this PropertyBuilder<TProperty> builder)
3026
{
31-
if (typeof(TProperty) != typeof(string) && typeof(TProperty) != typeof(byte[]))
32-
throw InvalidTypeException;
33-
3427
builder.HasAnnotation(IsEncryptedAnnotationName, true);
3528
return builder;
3629
}
@@ -54,9 +47,6 @@ public static PropertyBuilder<TProperty> IsEncrypted<TProperty>(this PropertyBui
5447
/// <returns>The same builder instance so that multiple configuration calls can be chained.</returns>
5548
public static PropertyBuilder<TProperty> IsEncryptedQueryable<TProperty>(this PropertyBuilder<TProperty> builder, bool isUnique = true)
5649
{
57-
if (typeof(TProperty) != typeof(string) && typeof(TProperty) != typeof(byte[]))
58-
throw InvalidTypeException;
59-
6050
builder.HasAnnotation(IsEncryptedAnnotationName, true);
6151
builder.HasAnnotation(IsQueryableAnnotationName, true);
6252
builder.HasAnnotation(IsUniqueIndexAnnotationName, isUnique);

src/Klean.EntityFrameworkCore.DataProtection/ValueConverters/ByteArrayDataProtectionValueConverter.cs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,24 @@
33

44
namespace EntityFrameworkCore.DataProtection.ValueConverters;
55

6-
/// <summary>
7-
/// Value converter for string properties that uses data protection to encrypt and decrypt the value.
8-
/// </summary>
9-
public sealed class ByteArrayDataProtectionValueConverter : ValueConverter<byte[], byte[]>
6+
internal sealed class ByteArrayDataProtectionValueConverter<T> : ValueConverter<T, byte[]>
7+
where T : notnull
108
{
119
/// <summary>
12-
/// Initializes a new instance of <see cref="ByteArrayDataProtectionValueConverter"/>.
10+
/// Initializes a new instance of <see cref="ByteArrayDataProtectionValueConverter{T}"/>.
1311
/// </summary>
1412
/// <param name="protector">the data protector used to protect and unprotect the data</param>
1513
/// <param name="internalConverter">the internal value converter to use</param>
1614
public ByteArrayDataProtectionValueConverter(IDataProtector protector, ValueConverter? internalConverter = null) : base(
17-
to => internalConverter == null ? protector.Protect(to) : protector.Protect((byte[])internalConverter.ConvertToProvider(to)!),
18-
from => internalConverter == null ? protector.Unprotect(from) : (byte[])internalConverter.ConvertFromProvider(protector.Unprotect(from))!)
15+
to => internalConverter == null ? protector.Protect((byte[])(object)to) : protector.Protect((byte[])internalConverter.ConvertToProvider(to)!),
16+
from => internalConverter == null ? (T)(object)protector.Unprotect(from) : (T)internalConverter.ConvertFromProvider(protector.Unprotect(from))!)
1917
{
2018
if (internalConverter is null) return;
2119

22-
var genericArguments = internalConverter.GetType().GetGenericArguments();
20+
var genericArguments = internalConverter.GetType().BaseType!.GetGenericArguments();
2321
var convertTo = genericArguments[1];
2422

2523
if (convertTo != typeof(byte[]))
2624
throw new InvalidOperationException("The internal value converter must convert your type to a byte[] to be used as an intermediary converter.");
2725
}
28-
}
26+
}

src/Klean.EntityFrameworkCore.DataProtection/ValueConverters/StringDataProtectionValueConverter.cs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,21 @@
33

44
namespace EntityFrameworkCore.DataProtection.ValueConverters;
55

6-
/// <summary>
7-
/// Value converter for string properties that uses data protection to encrypt and decrypt the value.
8-
/// </summary>
9-
public sealed class StringDataProtectionValueConverter : ValueConverter<string, string>
6+
internal sealed class StringDataProtectionValueConverter<T> : ValueConverter<T, string>
7+
where T : notnull
108
{
119
/// <summary>
12-
/// Initializes a new instance of <see cref="StringDataProtectionValueConverter"/>.
10+
/// Initializes a new instance of <see cref="StringDataProtectionValueConverter{T}"/>.
1311
/// </summary>
1412
/// <param name="protector">the data protector used to protect and unprotect the data</param>
1513
/// <param name="internalConverter">the internal value converter to use</param>
1614
public StringDataProtectionValueConverter(IDataProtector protector, ValueConverter? internalConverter = null) : base(
17-
to => internalConverter == null ? protector.Protect(to) : protector.Protect((string)internalConverter.ConvertToProvider(to)!),
18-
from => internalConverter == null ? protector.Unprotect(from) : (string)internalConverter.ConvertFromProvider(protector.Unprotect(from))!)
15+
to => internalConverter == null ? protector.Protect((string)(object)to) : protector.Protect((string)internalConverter.ConvertToProvider(to)!),
16+
from => internalConverter == null ? (T)(object)protector.Unprotect(from) : (T)internalConverter.ConvertFromProvider(protector.Unprotect(from))!)
1917
{
2018
if (internalConverter is null) return;
2119

22-
var genericArguments = internalConverter.GetType().GetGenericArguments();
20+
var genericArguments = internalConverter.GetType().BaseType!.GetGenericArguments();
2321
var convertTo = genericArguments[1];
2422

2523
if (convertTo != typeof(string))

test/Klean.EntityFrameworkCore.DataProtection.Test/ValueConverters.Test.cs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using EntityFrameworkCore.DataProtection.ValueConverters;
33
using FluentAssertions;
44
using Microsoft.AspNetCore.DataProtection;
5+
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
56
using Microsoft.Extensions.DependencyInjection;
67

78
namespace Klean.EntityFrameworkCore.DataProtection.Test;
@@ -15,8 +16,8 @@ public void Test_ValueConverters()
1516
var dataProtectorProvider = scope.ServiceProvider.GetRequiredService<IDataProtectionProvider>();
1617
var protector = dataProtectorProvider.CreateProtector("Klean.EntityFrameworkCore.DataProtection");
1718

18-
var stringConverter = new StringDataProtectionValueConverter(protector);
19-
var byteArrayConverter = new ByteArrayDataProtectionValueConverter(protector);
19+
var stringConverter = new StringDataProtectionValueConverter<string>(protector);
20+
var byteArrayConverter = new ByteArrayDataProtectionValueConverter<byte[]>(protector);
2021

2122
const string payload = "Hello, World!";
2223
var encryptedString = stringConverter.ConvertToProvider(payload);
@@ -30,4 +31,28 @@ public void Test_ValueConverters()
3031
var decryptedBytes = byteArrayConverter.ConvertFromProvider(encryptedBytes);
3132
decryptedBytes.Should().BeEquivalentTo(payloadBytes);
3233
}
34+
35+
record Wrapper(string Value);
36+
37+
class IntermediaryConverter() : ValueConverter<Wrapper, string>(
38+
to => to.Value,
39+
from => new Wrapper(from));
40+
41+
[Test]
42+
public void Test_IntermediaryConverters()
43+
{
44+
using var scope = Util.ServiceProvider.Value.CreateScope();
45+
var dataProtectorProvider = scope.ServiceProvider.GetRequiredService<IDataProtectionProvider>();
46+
var protector = dataProtectorProvider.CreateProtector("Klean.EntityFrameworkCore.DataProtection");
47+
48+
var intermediaryConverter = new IntermediaryConverter();
49+
var stringConverter = new StringDataProtectionValueConverter<Wrapper>(protector, intermediaryConverter);
50+
51+
const string payload = "Hello, World!";
52+
var value = new Wrapper(payload);
53+
var encrypted = stringConverter.ConvertToProvider(value);
54+
Console.WriteLine(encrypted);
55+
var decryptedString = stringConverter.ConvertFromProvider(encrypted);
56+
decryptedString.Should().Be(value);
57+
}
3358
}

0 commit comments

Comments
 (0)