Skip to content

Commit 425fc88

Browse files
committed
allowed properties to be marked as unique
1 parent 3ca2f9d commit 425fc88

File tree

13 files changed

+188
-139
lines changed

13 files changed

+188
-139
lines changed

.github/FUNDING

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
github: https://github.com/ddjerqq

.github/workflows/build.yaml

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,57 @@
1-
name: .NET
1+
name: Build and publish
22

33
on:
44
workflow_dispatch:
5-
6-
#on:
75
# push:
86
# branches: [ main ]
97
# pull_request:
108
# branches: [ main ]
119

10+
env:
11+
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
12+
DOTNET_CLI_TELEMETRY_OPTOUT: true
13+
1214
jobs:
1315
build-library:
16+
concurrency: ci-${{ github.ref }}
1417
runs-on: ubuntu-latest
1518

1619
steps:
17-
- name: Checkout repository
20+
- name: Checkout 🛎️
1821
uses: actions/checkout@v4
1922

20-
- name: Setup .NET Core
23+
- name: Setup .NET 🛠️
2124
uses: actions/setup-dotnet@v4
2225
with:
2326
dotnet-version: |
24-
3.1.x
25-
5.0.x
2627
6.0.x
2728
7.0.x
2829
8.0.x
2930
9.0.x
3031
31-
- name: Display .NET version
32+
- name: Load cache 🔃
33+
id: nuget-cache
34+
uses: actions/cache@v4
35+
with:
36+
path: "~/.nuget/packages"
37+
key: "${{ runner.os }}-nuget-api-${{ hashFiles('**/*.csproj') }}"
38+
restore-keys: "${{ runner.os }}-nuget-api-"
39+
40+
- name: Cache status ✅
41+
run: |
42+
echo "Cache hit: ${{ steps.nuget-cache.outputs.cache-hit }}"
43+
44+
- name: Display .NET version 📚
3245
run: dotnet --version
3346

34-
- name: Restore dependencies
35-
run: dotnet restore
47+
- name: Restore 📦
48+
run: dotnet restore --verbosity minimal
3649

37-
- name: Build
38-
run: dotnet build SoftFluent.EntityFrameworkCore.DataEncryption.sln --configuration Release -f net9.0 --no-restore
50+
- name: Test 🧪
51+
run: dotnet test --no-restore --verbosity minimal
3952

40-
- name: Run unit tests
41-
run: dotnet test --configuration Release --collect:"XPlat Code Coverage" --settings ./test/EntityFrameworkCore.DataEncryption.Test/runsettings.xml
53+
- name: Pack 📦
54+
run: dotnet pack --configuration Release --output .
4255

43-
- name: Copy coverage results
44-
run: cp ./test/EntityFrameworkCore.DataEncryption.Test/TestResults/**/*.xml ./test/EntityFrameworkCore.DataEncryption.Test/TestResults/
56+
- name: Push to NuGet 🚀
57+
run: dotnet nuget push "*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source "https://api.nuget.org/v3/index.json"

EntityFrameworkCore.DataProtection.sln

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "src/EntityFrameworkCore.DataProtection", "src/EntityFrameworkCore.DataProtection\EntityFrameworkCore.DataProtection.csproj", "{23817096-829D-465B-9C81-39EF87D90468}"
3+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "src/EntityFrameworkCore.DataProtection", "src/EntityFrameworkCore.DataProtection/EntityFrameworkCore.DataProtection.csproj", "{23817096-829D-465B-9C81-39EF87D90468}"
44
EndProject
55
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{08CF6BCC-3928-4FB6-A1A0-ABF5DE162D6C}"
66
EndProject
77
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{6A01C6D2-B6BE-4584-897B-60710734DC66}"
88
EndProject
9-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkCore.DataProtection.Test", "test\EntityFrameworkCore.DataProtection.Test\EntityFrameworkCore.DataProtection.Test.csproj", "{AEACE4E1-D3F5-4A5D-8632-695AFDBDC020}"
9+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkCore.DataProtection.Test", "test/EntityFrameworkCore.DataProtection.Test/EntityFrameworkCore.DataProtection.Test.csproj", "{AEACE4E1-D3F5-4A5D-8632-695AFDBDC020}"
1010
EndProject
1111
Global
1212
GlobalSection(SolutionConfigurationPlatforms) = preSolution

README.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -110,35 +110,41 @@ Using the EncryptedAttribute:
110110
```csharp
111111
class User
112112
{
113-
[Encrypt(isQueryable: true)]
113+
// v default v default
114+
[Encrypt(IsQueryable = true, IsUnique = true)]
114115
public string SocialSecurityNumber { get; set; }
115116

116-
[Encrypt]
117+
[Encrypt(IsQueryable = false, IsUnique = false)]
117118
public byte[] IdPicture { get; set; }
118119
}
119120
```
120121

122+
> [!TIP]
123+
> By default `IsQueryable` and `IsUnique` are set to `true`, you can omit them if you want to use the default values.
124+
> If you have a property that is marked as `IsQueryable = true` and you want to query it, you **MUST** call `AddDataProtectionInterceptors` in your `DbContext` configuration.
125+
121126
Using the FluentApi (in your `DbContext.OnModelCreating` method):
122127
```csharp
123128
protected override void OnModelCreating(ModelBuilder builder)
124129
{
125-
builder.Entity<User>(entity =>
126-
{
127-
entity.Property(e => e.SocialSecurityNumber).IsEncrypted(isQueryable: true);
128-
entity.Property(e => e.IdPicture).IsEncrypted(isQueryable: false);
129-
});
130+
builder.Entity<User>(entity =>
131+
{
132+
// v defaults to true
133+
entity.Property(e => e.SocialSecurityNumber).IsEncryptedQueryable(isUnique: true);
134+
entity.Property(e => e.IdPicture).IsEncrypted();
135+
});
130136
}
131137
```
132138

133139
Creating a custom `EntityTypeConfiguration` (Recommended for DDD):
134140
```csharp
135141
class UserConfiguration : IEntityTypeConfiguration<User>
136142
{
137-
public void Configure(EntityTypeBuilder<User> builder)
138-
{
139-
builder.Property(e => e.SocialSecurityNumber).IsEncrypted(isQueryable: true);
140-
builder.Property(e => e.IdPicture).IsEncrypted(isQueryable: false);
141-
}
143+
public void Configure(EntityTypeBuilder<User> builder)
144+
{
145+
builder.Property(e => e.SocialSecurityNumber).IsEncryptedQueryable();
146+
builder.Property(e => e.IdPicture).IsEncrypted();
147+
}
142148
}
143149
```
144150

src/EntityFrameworkCore.DataProtection/Resources/icon.png renamed to Resources/icon.png

File renamed without changes.

src/EntityFrameworkCore.DataProtection/EncryptAttribute.cs

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,23 @@
22

33
/// <summary>
44
/// Marks a property as encrypted.
5+
/// Optionally choose if you want your property to be queryable or not.
6+
/// Optionally choose if you want your properties to have a Unique Index or not.
57
/// </summary>
68
/// <remarks>
7-
/// Because of how data protection in this library is implemented, if you want your protected property to be queryable, you must ensure that:
8-
/// <para></para>
9-
/// A) The property is a string or byte[].
10-
/// <para></para>
11-
/// B) The values of this property are unique (email addresses, full names, so on).
9+
/// Because of how data protection in this library is implemented, if you want your protected property to be queryable, you must ensure that the property is a string or byte[].
1210
/// </remarks>
1311
[AttributeUsage(AttributeTargets.Property)]
1412
public sealed class EncryptAttribute : Attribute
1513
{
1614
/// <summary>
1715
/// Gets a boolean value indicating if this property can be queried from the database.
18-
/// Because of how data protection in this library is implemented, if you want your protected property to be queryable, you must ensure that:
19-
/// <para></para>
20-
/// A) The property is a string or byte[].
21-
/// <para></para>
22-
/// B) The values of this property are unique (email addresses, full names, so on).
16+
/// Because of how data protection in this library is implemented, if you want your protected property to be queryable, you must ensure that the property is a string or byte[].
2317
/// </summary>
24-
public bool IsQueryable { get; }
18+
public bool IsQueryable { get; init; } = true;
2519

2620
/// <summary>
27-
/// Initializes a new instance of the <see cref="EncryptAttribute"/> class.
28-
/// indicates if this property can be queried from the database.
29-
/// Because of how data protection in this library is implemented, if you want your protected property to be queryable, you must ensure that:
30-
/// <para></para>
31-
/// A) The property is a string or byte[].
32-
/// <para></para>
33-
/// B) The values of this property are unique (email addresses, full names, so on).
21+
/// Gets a boolean value indicating if this property should have a unique index.
3422
/// </summary>
35-
public EncryptAttribute(bool isQueryable = false) => IsQueryable = isQueryable;
23+
public bool IsUnique { get; init; } = true;
3624
}

src/EntityFrameworkCore.DataProtection/EntityFrameworkCore.DataProtection.csproj

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77
<LangVersion>10.0</LangVersion>
8-
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
8+
<Version>1.0.0</Version>
99
<AssemblyName>EntityFrameworkCore.DataProtection</AssemblyName>
1010
<RootNamespace>EntityFrameworkCore.DataProtection</RootNamespace>
1111
<IsPackable>true</IsPackable>
12-
<Version>8.0.11</Version>
1312
<Authors>ddjerqq</Authors>
1413
<Company>ddjerqq</Company>
1514
<PackageId>EntityFrameworkCore.DataProtection</PackageId>
@@ -18,30 +17,39 @@
1817
<RepositoryType>git</RepositoryType>
1918
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
2019
<GenerateDocumentationFile>true</GenerateDocumentationFile>
21-
<PackageTags>entity-framework-core,extensions,dotnet-core,dotnet,encryption,data-protection,fluent-api</PackageTags>
22-
<PackageIcon>Resources/icon.png</PackageIcon>
23-
<Copyright>ddjerqq © 2019 - 2024</Copyright>
24-
<Description>A plugin for Microsoft.EntityFrameworkCore to add support of data protection with built-in data protection providers</Description>
25-
<PackageLicenseFile>LICENSE</PackageLicenseFile>
20+
<PackageTags>entity-framework-core,extensions,dotnet-core,dotnet,encryption,data-protection</PackageTags>
21+
<PackageIcon>icon.png</PackageIcon>
22+
<Copyright>ddjerqq © 2024 - 2028</Copyright>
23+
<Description>A plugin for Microsoft.EntityFrameworkCore that adds support of data protection, while maintaining queryability for encrypted properties.</Description>
2624
<PackageReleaseNotes>https://github.com/ddjerqq/EntityFrameworkCore.DataEncryption/releases/</PackageReleaseNotes>
25+
<PackageLicenseFile>LICENSE</PackageLicenseFile>
2726
<PackageReadmeFile>README.md</PackageReadmeFile>
2827
</PropertyGroup>
2928

30-
<ItemGroup>
31-
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
32-
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.11" />
29+
<ItemGroup Condition="('$(TargetFramework)' == 'net6.0')">
30+
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="[6.0.36,8)"/>
31+
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="[6.0.36,8)"/>
32+
</ItemGroup>
33+
34+
<ItemGroup Condition="('$(TargetFramework)' == 'net7.0')">
35+
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="[7,9)"/>
36+
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="[7,9)"/>
37+
</ItemGroup>
38+
39+
<ItemGroup Condition="('$(TargetFramework)' == 'net8.0')">
40+
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="[8.0.11,)"/>
41+
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="[8.0.11,)"/>
42+
</ItemGroup>
43+
44+
<ItemGroup Condition="('$(TargetFramework)' == 'net9.0')">
45+
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="[9,)"/>
46+
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="[9,)"/>
3347
</ItemGroup>
3448

3549
<ItemGroup>
36-
<None Include="../../LICENSE">
37-
<Pack>True</Pack>
38-
<PackagePath>/</PackagePath>
39-
</None>
40-
<None Include="../../README.md">
41-
<Pack>True</Pack>
42-
<PackagePath>/</PackagePath>
43-
</None>
44-
<None Include="Resources/icon.png" Pack="true" Visible="true" PackagePath="" />
50+
<None Include="../../LICENSE" Pack="true" PackagePath="/"/>
51+
<None Include="../../README.md" Pack="true" PackagePath="/"/>
52+
<None Include="../../Resources/icon.png" Pack="true" Visible="true" PackagePath="/"/>
4553
</ItemGroup>
4654

4755
<ItemGroup>

src/EntityFrameworkCore.DataProtection/Extensions/ModelBuilderExt.cs

Lines changed: 22 additions & 18 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;
5+
using Microsoft.EntityFrameworkCore.Metadata.Builders;
56

67
namespace EntityFrameworkCore.DataProtection.Extensions;
78

@@ -12,7 +13,7 @@ public static class ModelBuilderExt
1213
{
1314
/// <summary>
1415
/// Configures the data protection for the entities marked with <see cref="EncryptAttribute"/> or Fluently marked
15-
/// as Encrypted using <see cref="PropertyBuilderExtensions.IsEncrypted{TProperty}"/>.
16+
/// as Encrypted using <see cref="PropertyBuilderExt.IsEncrypted{TProperty}"/>.
1617
/// </summary>
1718
/// <remarks>
1819
/// You must call this method __after__ <see cref="ModelBuilder.ApplyConfigurationsFromAssembly"/>.
@@ -49,14 +50,14 @@ public static ModelBuilder UseDataProtection(this ModelBuilder builder, IDataPro
4950
var protector = dataProtectionProvider.CreateProtector("EntityFrameworkCore.DataProtection");
5051

5152
var properties = (
52-
from prop in builder.Model.GetEntityTypes().SelectMany(type => type.GetProperties())
53-
let status = prop.GetEncryptionStatus()
53+
from entityType in builder.Model.GetEntityTypes()
54+
from prop in entityType.GetProperties()
55+
let status = prop.GetEncryptionMetadata()
5456
where status.SupportsEncryption
55-
select (prop, status.SupportsQuerying))
56-
// need to collect to a list because it is modified in AddShadowProperty
57+
select (entityType, prop, status))
5758
.ToList();
5859

59-
foreach (var (property, supportsQuerying) in properties)
60+
foreach (var (entityType, property, status) in properties)
6061
{
6162
var propertyType = property.PropertyInfo?.PropertyType;
6263

@@ -65,28 +66,31 @@ where status.SupportsEncryption
6566
else if (propertyType == typeof(byte[]))
6667
property.SetValueConverter(new ByteArrayDataProtectionValueConverter(protector));
6768
else
68-
throw new InvalidOperationException("Only string and byte[] properties are supported for now. Please open an issue on https://github.com/ddjerqq/EntityFrameworkCore.DataProtection/issues to request a new feature");
69+
throw PropertyBuilderExt.InvalidTypeException;
6970

70-
if (supportsQuerying)
71-
AddShadowProperty(property);
71+
if (status.SupportsQuerying)
72+
AddShadowProperty(entityType, property, status.IsUniqueIndex);
7273
}
7374

7475
return builder;
7576
}
7677

77-
private static void AddShadowProperty(IMutableProperty property)
78+
private static void AddShadowProperty(IMutableEntityType entityType, IMutableProperty property, bool isUniqueIndex)
7879
{
79-
var entityType = property.DeclaringType;
8080
var originalPropertyName = property.Name;
8181
var shadowPropertyName = $"{originalPropertyName}ShadowHash";
8282

83-
if (entityType.GetProperties().All(p => p.Name != shadowPropertyName))
84-
{
85-
var shadowProperty = entityType.AddProperty(shadowPropertyName, typeof(string));
86-
shadowProperty.IsShadowProperty();
87-
// TODO: do we need the shadow hashes to be unique indexes?
83+
if (entityType.GetProperties().Any(p => p.Name == shadowPropertyName))
84+
return;
85+
86+
var shadowProperty = entityType.AddProperty(shadowPropertyName, typeof(string));
87+
shadowProperty.IsShadowProperty();
88+
89+
if (isUniqueIndex)
8890
shadowProperty.IsUniqueIndex();
89-
shadowProperty.IsNullable = property.IsNullable;
90-
}
91+
else
92+
shadowProperty.IsIndex();
93+
94+
shadowProperty.IsNullable = property.IsNullable;
9195
}
9296
}

0 commit comments

Comments
 (0)