Skip to content

Commit 8b89e1c

Browse files
committed
added support for intermediary converters
1 parent d343819 commit 8b89e1c

File tree

9 files changed

+95
-15
lines changed

9 files changed

+95
-15
lines changed

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
[![Nuget](https://img.shields.io/nuget/v/Klean.EntityFrameworkCore.DataProtection.svg)](https://www.nuget.org/packages/Klean.EntityFrameworkCore.DataProtection)
55
[![Nuget Downloads](https://img.shields.io/nuget/dt/Klean.EntityFrameworkCore.DataProtection)](https://www.nuget.org/packages/Klean.EntityFrameworkCore.DataProtection)
66

7+
![#](./Resources/efcoredp-gh-social-cover.png "ef core data protection")
8+
79
`Klean.EntityFrameworkCore.DataProtection` is a [Microsoft Entity Framework Core](https://github.com/aspnet/EntityFrameworkCore) extension which
810
adds support for data protection and querying for encrypted properties for your entities.
911

@@ -159,6 +161,42 @@ var foo = await DbContext.Users
159161
160162
### Profit!
161163

164+
---
165+
166+
## Esoteric usage:
167+
168+
> Q: How to use intermediary converters? <br/>
169+
> A: If you have an entity that needs a custom converter, you are covered, all you have to do is specify that the property has a custom converter along with the `IsEncrypted` attribute.
170+
```csharp
171+
sealed record AddressData(string Country, string ZipCode)
172+
{
173+
public static AddressData Parse(string str) => str.Split('-') switch
174+
{
175+
[var country, var zipCode] => new AddressData(country, zipCode),
176+
_ => throw new FormatException("Invalid format"),
177+
};
178+
179+
public override string ToString() => $"{Country}-{ZipCode}";
180+
}
181+
182+
// intermediary converter
183+
class AddressToStringIntermediaryConverter() : ValueConverter<AddressData, string>(
184+
to => to.ToString(),
185+
from => AddressData.Parse(from));
186+
187+
// user configuration
188+
class UserConfiguration : IEntityTypeConfiguration<User>
189+
{
190+
public void Configure(EntityTypeBuilder<User> builder)
191+
{
192+
builder.Property(x => x.Address)
193+
// the order does not matter.
194+
.HasConversion<AddressToStringIntermediaryConverter>()
195+
.IsEncrypted();
196+
}
197+
}
198+
```
199+
162200
## Thank you for using this library!
163201

164202
ddjerqq <3
590 KB
Loading

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,12 @@ where status.SupportsEncryption
6060
foreach (var (entityType, property, status) in properties)
6161
{
6262
var propertyType = property.PropertyInfo?.PropertyType;
63+
var internalConverter = property.GetValueConverter();
6364

6465
if (propertyType == typeof(string))
65-
property.SetValueConverter(new StringDataProtectionValueConverter(protector));
66+
property.SetValueConverter(new StringDataProtectionValueConverter(protector, internalConverter));
6667
else if (propertyType == typeof(byte[]))
67-
property.SetValueConverter(new ByteArrayDataProtectionValueConverter(protector));
68+
property.SetValueConverter(new ByteArrayDataProtectionValueConverter(protector, internalConverter));
6869
else
6970
throw PropertyBuilderExt.InvalidTypeException;
7071

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,18 @@ public sealed class ByteArrayDataProtectionValueConverter : ValueConverter<byte[
1111
/// <summary>
1212
/// Initializes a new instance of <see cref="ByteArrayDataProtectionValueConverter"/>.
1313
/// </summary>
14-
/// <param name="protector"></param>
15-
public ByteArrayDataProtectionValueConverter(IDataProtector protector) : base(
16-
to => protector.Protect(to),
17-
from => protector.Unprotect(from))
14+
/// <param name="protector">the data protector used to protect and unprotect the data</param>
15+
/// <param name="internalConverter">the internal value converter to use</param>
16+
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))!)
1819
{
20+
if (internalConverter is null) return;
21+
22+
var genericArguments = internalConverter.GetType().GetGenericArguments();
23+
var convertTo = genericArguments[1];
24+
25+
if (convertTo != typeof(byte[]))
26+
throw new InvalidOperationException("The internal value converter must convert your type to a byte[] to be used as an intermediary converter.");
1927
}
2028
}

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,18 @@ public sealed class StringDataProtectionValueConverter : ValueConverter<string,
1111
/// <summary>
1212
/// Initializes a new instance of <see cref="StringDataProtectionValueConverter"/>.
1313
/// </summary>
14-
/// <param name="protector"></param>
15-
public StringDataProtectionValueConverter(IDataProtector protector) : base(
16-
to => protector.Protect(to),
17-
from => protector.Unprotect(from))
14+
/// <param name="protector">the data protector used to protect and unprotect the data</param>
15+
/// <param name="internalConverter">the internal value converter to use</param>
16+
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))!)
1819
{
20+
if (internalConverter is null) return;
21+
22+
var genericArguments = internalConverter.GetType().GetGenericArguments();
23+
var convertTo = genericArguments[1];
24+
25+
if (convertTo != typeof(string))
26+
throw new InvalidOperationException("The internal value converter must convert your type to a string to be used as an intermediary converter.");
1927
}
2028
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
namespace Klean.EntityFrameworkCore.DataProtection.Test.Data;
2+
3+
public sealed record AddressData
4+
{
5+
public string Country { get; init; }
6+
public string ZipCode { get; init; }
7+
8+
public static AddressData Parse(string str) => str.Split('-') switch
9+
{
10+
[var country, var zipCode] => new AddressData { Country = country, ZipCode = zipCode },
11+
_ => throw new FormatException("Invalid format"),
12+
};
13+
14+
public override string ToString() => $"{Country}-{ZipCode}";
15+
}

test/Klean.EntityFrameworkCore.DataProtection.Test/Data/TestDbContext.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using Microsoft.AspNetCore.DataProtection;
33
using Microsoft.EntityFrameworkCore;
44
using Microsoft.EntityFrameworkCore.Metadata.Builders;
5+
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
56

67
namespace Klean.EntityFrameworkCore.DataProtection.Test.Data;
78

@@ -22,5 +23,12 @@ internal class UserConfiguration : IEntityTypeConfiguration<User>
2223
public void Configure(EntityTypeBuilder<User> builder)
2324
{
2425
builder.Property(x => x.Email).IsEncryptedQueryable();
26+
builder.Property(x => x.Address)
27+
.HasConversion<AddressToStringIntermediaryConverter>()
28+
.IsEncrypted();
2529
}
30+
31+
private class AddressToStringIntermediaryConverter() : ValueConverter<AddressData, string>(
32+
to => to.ToString(),
33+
from => AddressData.Parse(from));
2634
}

test/Klean.EntityFrameworkCore.DataProtection.Test/Data/User.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ internal sealed class User
1717
/// </summary>
1818
public required string Email { get; init; }
1919

20+
public required AddressData Address { get; init; }
21+
2022
[Encrypt(IsQueryable = false, IsUnique = false)]
2123
public required byte[] IdPicture { get; init; }
2224

@@ -28,6 +30,7 @@ public static User CreateRandom()
2830
Name = RandomNumberGenerator.GetHexString(5),
2931
SocialSecurityNumber = RandomNumberGenerator.GetHexString(11),
3032
Email = RandomNumberGenerator.GetHexString(10),
33+
Address = new AddressData { Country = "GE", ZipCode = "42069" },
3134
IdPicture = RandomNumberGenerator.GetBytes(256),
3235
};
3336
}

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,17 @@ public void Test_Throws_WhenServicesAreNotRegistered()
2020
var connection = new SqliteConnection(Util.InMemoryDatabaseConnectionString);
2121
connection.Open();
2222

23-
// services.AddDataProtectionServices("test");
23+
services.AddDataProtectionServices("test");
2424
services.AddDbContext<BadDbContext>(opt => opt
2525
.AddDataProtectionInterceptors()
2626
.UseSqlite(connection));
2727

2828
var serviceProvider = services.BuildServiceProvider();
2929
using var scope = serviceProvider.CreateScope();
3030

31-
Assert.Throws<InvalidOperationException>(() =>
32-
{
33-
using var dbContext = scope.ServiceProvider.GetRequiredService<BadDbContext>();
34-
});
31+
using var dbContext = scope.ServiceProvider.GetRequiredService<BadDbContext>();
32+
33+
Assert.Pass();
3534
}
3635

3736
[Test]

0 commit comments

Comments
 (0)