Skip to content

Commit b329b0c

Browse files
author
Roman Golovanov
committed
fix invalid usage of serviceproviders
1 parent acc59f5 commit b329b0c

9 files changed

Lines changed: 126 additions & 52 deletions

File tree

src/EfCore.EncryptedProperties.Testing/DbContextOptionsBuilderTestExtensions.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using EfCore.EncryptedProperties.Extensions;
22
using Microsoft.EntityFrameworkCore;
3-
using Microsoft.EntityFrameworkCore.Diagnostics;
43
using Microsoft.Extensions.DependencyInjection;
54

65
namespace EfCore.EncryptedProperties.Testing;
@@ -24,7 +23,6 @@ public static DbContextOptionsBuilder UseEncryptedPropertiesForTesting(
2423
this DbContextOptionsBuilder builder,
2524
IServiceProvider serviceProvider)
2625
{
27-
builder.ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning));
2826
return builder.UseEncryptedProperties(serviceProvider);
2927
}
3028
}

src/EfCore.EncryptedProperties/EfCore.EncryptedProperties.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<RootNamespace>EfCore.EncryptedProperties</RootNamespace>
77
<PackageId>EfCore.EncryptedProperties</PackageId>
88
<Title>EF Core Encrypted Properties</Title>
9-
<Version>1.0.3</Version>
9+
<Version>1.0.4</Version>
1010
<Authors>RomanGolovanov</Authors>
1111
<Description>Property-level encryption for Entity Framework Core using AES-GCM data encryption keys and RSA-wrapped key encryption keys.</Description>
1212
<PackageTags>entity-framework-core;ef-core;encryption;aes-gcm;key-vault;privacy</PackageTags>
@@ -15,7 +15,7 @@
1515
<RepositoryType>git</RepositoryType>
1616
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
1717
<PackageReadmeFile>README.md</PackageReadmeFile>
18-
<PackageReleaseNotes>Add multi-targeted package assets for .NET 8, .NET 9, and .NET 10 with matching EF Core dependency groups.</PackageReleaseNotes>
18+
<PackageReleaseNotes>Changes since 1.0.2:&#10;- Added X509 certificate store RSA key provider support.&#10;- Added file-based RSA key ring providers for raw RSA keys and password-protected PFX key material.&#10;- Added file-based and Azure Blob-based key chain storage providers.&#10;- Added Azure Blob-backed RSA key ring providers for RSA and PFX key material, plus an Azurite blob sample.&#10;- Fixed EF Core many-service-providers warning by reworking `UseEncryptedProperties` interceptor/provider wiring.&#10;- Added multi-targeted package assets for .NET 8, .NET 9, and .NET 10 with matching EF Core dependency groups.</PackageReleaseNotes>
1919
<PackageOutputPath>$(MSBuildProjectDirectory)/../../artifacts/packages</PackageOutputPath>
2020
<IncludeSymbols>true</IncludeSymbols>
2121
<SymbolPackageFormat>snupkg</SymbolPackageFormat>

src/EfCore.EncryptedProperties/Extensions/DbContextOptionsBuilderExtensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ public static DbContextOptionsBuilder UseEncryptedProperties(
1717
ArgumentNullException.ThrowIfNull(serviceProvider);
1818

1919
_ = serviceProvider.GetRequiredService<IEncryptedPropertyCryptor>();
20-
var extension = new EncryptedPropertiesDbContextOptionsExtension();
20+
var extension = new EncryptedPropertiesDbContextOptionsExtension(serviceProvider);
2121
((IDbContextOptionsBuilderInfrastructure)builder).AddOrUpdateExtension(extension);
2222

2323
builder.AddInterceptors(
24-
serviceProvider.GetRequiredService<EncryptedPropertiesSaveChangesInterceptor>(),
25-
serviceProvider.GetRequiredService<EncryptedPropertiesMaterializationInterceptor>());
24+
EncryptedPropertiesSaveChangesInterceptor.Instance,
25+
EncryptedPropertiesMaterializationInterceptor.Instance);
2626

2727
return builder;
2828
}

src/EfCore.EncryptedProperties/Extensions/EncryptedPropertiesServiceBuilder.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
using EfCore.EncryptedProperties.Configuration;
88
using EfCore.EncryptedProperties.Cryptography;
99
using EfCore.EncryptedProperties.Infrastructure;
10-
using EfCore.EncryptedProperties.Interceptors;
1110
using EfCore.EncryptedProperties.KeyManagement;
1211
using EfCore.EncryptedProperties.Metadata;
1312
using EfCore.EncryptedProperties.Providers;
@@ -39,8 +38,6 @@ public static IServiceCollection AddEncryptedProperties(
3938
services.RemoveAll<IEncryptedPropertyCryptor>();
4039
services.RemoveAll<EncryptedPropertyStateTracker>();
4140
services.RemoveAll<EncryptedPropertyModelCache>();
42-
services.RemoveAll<EncryptedPropertiesSaveChangesInterceptor>();
43-
services.RemoveAll<EncryptedPropertiesMaterializationInterceptor>();
4441

4542
services.AddLogging();
4643
services.AddSingleton(options);
@@ -49,8 +46,6 @@ public static IServiceCollection AddEncryptedProperties(
4946
services.AddSingleton<IEncryptedPropertyCryptor, EncryptedPropertyCryptor>();
5047
services.AddScoped<EncryptedPropertyStateTracker>();
5148
services.AddSingleton<EncryptedPropertyModelCache>();
52-
services.AddSingleton<EncryptedPropertiesSaveChangesInterceptor>();
53-
services.AddSingleton<EncryptedPropertiesMaterializationInterceptor>();
5449

5550
RemoveKeyChainPreloadHostedService(services);
5651
if (builder.PreloadOnStartup)

src/EfCore.EncryptedProperties/Infrastructure/EncryptedPropertiesDbContextOptionsExtension.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ internal sealed class EncryptedPropertiesDbContextOptionsExtension : IDbContextO
88
{
99
private DbContextOptionsExtensionInfo? _info;
1010

11+
public EncryptedPropertiesDbContextOptionsExtension(IServiceProvider applicationServiceProvider)
12+
{
13+
ApplicationServiceProvider = applicationServiceProvider;
14+
}
15+
16+
internal IServiceProvider ApplicationServiceProvider { get; }
17+
1118
public DbContextOptionsExtensionInfo Info => _info ??= new ExtensionInfo(this);
1219

1320
public void ApplyServices(IServiceCollection services)

src/EfCore.EncryptedProperties/Interceptors/EncryptedPropertiesMaterializationInterceptor.cs

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,24 @@ namespace EfCore.EncryptedProperties.Interceptors;
1111

1212
internal sealed class EncryptedPropertiesMaterializationInterceptor : IMaterializationInterceptor
1313
{
14-
private readonly IEncryptedPropertyCryptor _cryptor;
15-
private readonly EncryptedPropertyModelCache _modelCache;
14+
internal static readonly EncryptedPropertiesMaterializationInterceptor Instance = new();
1615

17-
public EncryptedPropertiesMaterializationInterceptor(
18-
IEncryptedPropertyCryptor cryptor,
19-
EncryptedPropertyModelCache? modelCache = null)
16+
private EncryptedPropertiesMaterializationInterceptor()
2017
{
21-
_cryptor = cryptor;
22-
_modelCache = modelCache ?? new EncryptedPropertyModelCache();
2318
}
2419

2520
public object InitializedInstance(MaterializationInterceptionData materializationData, object entity)
2621
{
2722
var context = materializationData.Context;
28-
var model = GetModel(context);
23+
var services = GetServices(context);
24+
var model = GetModel(context, services.ModelCache);
2925

3026
var entityTypeName = materializationData.EntityType.ClrType.FullName!;
3127
var descriptors = model.GetForEntityType(entityTypeName);
3228

3329
if (descriptors.Count == 0)
3430
return entity;
3531

36-
var services = GetServices(context);
37-
3832
foreach (var descriptor in descriptors)
3933
{
4034
var payload = GetCiphertextPayload(materializationData, descriptor);
@@ -104,20 +98,36 @@ private static EncryptedValueAccessors GetLazyAccessors(EncryptedPropertyDescrip
10498
$"Encrypted property '{descriptor.EntityTypeName}.{descriptor.PropertyName}' is not configured for lazy encrypted values.");
10599
}
106100

107-
private EncryptedPropertyServices GetServices(DbContext context)
101+
private static EncryptedPropertyServices GetServices(DbContext context)
108102
{
109-
var sp = ((IInfrastructure<IServiceProvider>)context).Instance;
103+
var efServices = ((IInfrastructure<IServiceProvider>)context).Instance;
104+
var applicationServices = GetApplicationServiceProvider(context);
105+
110106
return new EncryptedPropertyServices(
111-
_cryptor,
112-
sp.GetRequiredService<EncryptedPropertyStateTracker>());
107+
applicationServices.GetRequiredService<IEncryptedPropertyCryptor>(),
108+
applicationServices.GetRequiredService<EncryptedPropertyModelCache>(),
109+
efServices.GetRequiredService<EncryptedPropertyStateTracker>());
110+
}
111+
112+
private static IServiceProvider GetApplicationServiceProvider(DbContext context)
113+
{
114+
var extension = context.GetService<IDbContextOptions>()
115+
.FindExtension<EncryptedPropertiesDbContextOptionsExtension>();
116+
117+
return extension?.ApplicationServiceProvider
118+
?? throw new InvalidOperationException(
119+
"Encrypted properties are not configured for this DbContext. Call UseEncryptedProperties when configuring the DbContext options.");
113120
}
114121

115-
private EncryptedPropertyModel GetModel(DbContext context)
122+
private static EncryptedPropertyModel GetModel(
123+
DbContext context,
124+
EncryptedPropertyModelCache modelCache)
116125
{
117-
return _modelCache.GetOrAdd(context.Model);
126+
return modelCache.GetOrAdd(context.Model);
118127
}
119128

120129
private readonly record struct EncryptedPropertyServices(
121130
IEncryptedPropertyCryptor Cryptor,
131+
EncryptedPropertyModelCache ModelCache,
122132
EncryptedPropertyStateTracker StateTracker);
123133
}

src/EfCore.EncryptedProperties/Interceptors/EncryptedPropertiesSaveChangesInterceptor.cs

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,10 @@ namespace EfCore.EncryptedProperties.Interceptors;
1313

1414
internal sealed class EncryptedPropertiesSaveChangesInterceptor : SaveChangesInterceptor
1515
{
16-
private readonly IEncryptedPropertyCryptor _cryptor;
17-
private readonly EncryptedPropertyModelCache _modelCache;
16+
internal static readonly EncryptedPropertiesSaveChangesInterceptor Instance = new();
1817

19-
public EncryptedPropertiesSaveChangesInterceptor(
20-
IEncryptedPropertyCryptor cryptor,
21-
EncryptedPropertyModelCache? modelCache = null)
18+
private EncryptedPropertiesSaveChangesInterceptor()
2219
{
23-
_cryptor = cryptor;
24-
_modelCache = modelCache ?? new EncryptedPropertyModelCache();
2520
}
2621

2722
public override InterceptionResult<int> SavingChanges(
@@ -51,12 +46,11 @@ public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(
5146

5247
private void ProcessSavingChanges(DbContext context, CancellationToken cancellationToken)
5348
{
54-
var model = GetModel(context);
49+
var services = GetServices(context);
50+
var model = GetModel(context, services.ModelCache);
5551
if (model.Properties.Count == 0)
5652
return;
5753

58-
var services = GetServices(context);
59-
6054
foreach (var entry in GetProcessableEntries(context))
6155
{
6256
foreach (var descriptor in model.GetForEntityType(entry.Metadata.ClrType.FullName!))
@@ -81,12 +75,11 @@ private void ProcessSavingChanges(DbContext context, CancellationToken cancellat
8175

8276
private async Task ProcessSavingChangesAsync(DbContext context, CancellationToken cancellationToken)
8377
{
84-
var model = GetModel(context);
78+
var services = GetServices(context);
79+
var model = GetModel(context, services.ModelCache);
8580
if (model.Properties.Count == 0)
8681
return;
8782

88-
var services = GetServices(context);
89-
9083
foreach (var entry in GetProcessableEntries(context))
9184
{
9285
foreach (var descriptor in model.GetForEntityType(entry.Metadata.ClrType.FullName!))
@@ -361,20 +354,36 @@ private static EncryptedValueAccessors GetLazyAccessors(EncryptedPropertyDescrip
361354
$"Encrypted property '{descriptor.EntityTypeName}.{descriptor.PropertyName}' is not configured for lazy encrypted values.");
362355
}
363356

364-
private EncryptedPropertyServices GetServices(DbContext context)
357+
private static EncryptedPropertyServices GetServices(DbContext context)
365358
{
366-
var sp = ((IInfrastructure<IServiceProvider>)context).Instance;
359+
var efServices = ((IInfrastructure<IServiceProvider>)context).Instance;
360+
var applicationServices = GetApplicationServiceProvider(context);
361+
367362
return new EncryptedPropertyServices(
368-
_cryptor,
369-
sp.GetRequiredService<EncryptedPropertyStateTracker>());
363+
applicationServices.GetRequiredService<IEncryptedPropertyCryptor>(),
364+
applicationServices.GetRequiredService<EncryptedPropertyModelCache>(),
365+
efServices.GetRequiredService<EncryptedPropertyStateTracker>());
366+
}
367+
368+
private static IServiceProvider GetApplicationServiceProvider(DbContext context)
369+
{
370+
var extension = context.GetService<IDbContextOptions>()
371+
.FindExtension<EncryptedPropertiesDbContextOptionsExtension>();
372+
373+
return extension?.ApplicationServiceProvider
374+
?? throw new InvalidOperationException(
375+
"Encrypted properties are not configured for this DbContext. Call UseEncryptedProperties when configuring the DbContext options.");
370376
}
371377

372-
private EncryptedPropertyModel GetModel(DbContext context)
378+
private static EncryptedPropertyModel GetModel(
379+
DbContext context,
380+
EncryptedPropertyModelCache modelCache)
373381
{
374-
return _modelCache.GetOrAdd(context.Model);
382+
return modelCache.GetOrAdd(context.Model);
375383
}
376384

377385
private readonly record struct EncryptedPropertyServices(
378386
IEncryptedPropertyCryptor Cryptor,
387+
EncryptedPropertyModelCache ModelCache,
379388
EncryptedPropertyStateTracker StateTracker);
380389
}

tests/EfCore.EncryptedProperties.Tests/Infrastructure/EncryptedPropertiesServiceRegistrationTests.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using EfCore.EncryptedProperties.Infrastructure;
88
using EfCore.EncryptedProperties.KeyManagement;
99
using EfCore.EncryptedProperties.Providers;
10+
using Microsoft.EntityFrameworkCore;
1011
using Microsoft.Extensions.DependencyInjection;
1112
using Microsoft.Extensions.Hosting;
1213

@@ -67,6 +68,40 @@ public void AddEncryptedProperties_RegistersStateTracker_AsScoped()
6768
scope2.ServiceProvider.GetRequiredService<EncryptedPropertyStateTracker>());
6869
}
6970

71+
[Fact]
72+
public async Task UseEncryptedProperties_DoesNotCreateDistinctEfInternalServiceProvider_PerApplicationProvider()
73+
{
74+
for (var i = 0; i < 25; i++)
75+
{
76+
var rsaKeyId = $"rsa-v{i}";
77+
var services = new ServiceCollection();
78+
services.AddEncryptedProperties(cfg =>
79+
{
80+
cfg.WithInMemoryRsaKeyProvider(RSA.Create(2048), rsaKeyId);
81+
cfg.WithInMemoryKeyChain();
82+
});
83+
services.AddDbContext<CacheWarningDbContext>((sp, options) =>
84+
{
85+
options.UseInMemoryDatabase(Guid.NewGuid().ToString());
86+
options.UseEncryptedProperties(sp);
87+
});
88+
89+
using var provider = services.BuildServiceProvider();
90+
using var scope = provider.CreateScope();
91+
var context = scope.ServiceProvider.GetRequiredService<CacheWarningDbContext>();
92+
93+
context.Entities.Add(new CacheWarningEntity
94+
{
95+
Id = Guid.NewGuid(),
96+
Secret = "classified"
97+
});
98+
await context.SaveChangesAsync();
99+
100+
var records = await provider.GetRequiredService<IKeyChainStorage>().GetAllAsync();
101+
Assert.Contains(records, record => record.RsaKeyId == rsaKeyId);
102+
}
103+
}
104+
70105
[Fact]
71106
public void AddEncryptedProperties_MissingRsaKeyProvider_Throws()
72107
{
@@ -368,6 +403,31 @@ private sealed class NullConnectionFactory : DbProviderFactory
368403
private static BlobContainerClient CreateBlobContainerClient()
369404
=> new(new Uri("https://account.blob.core.windows.net/encrypted-properties-tests"));
370405

406+
private sealed class CacheWarningDbContext : DbContext
407+
{
408+
public CacheWarningDbContext(DbContextOptions<CacheWarningDbContext> options)
409+
: base(options)
410+
{
411+
}
412+
413+
public DbSet<CacheWarningEntity> Entities => Set<CacheWarningEntity>();
414+
415+
protected override void OnModelCreating(ModelBuilder modelBuilder)
416+
{
417+
modelBuilder.Entity<CacheWarningEntity>(entity =>
418+
{
419+
entity.HasKey(e => e.Id);
420+
entity.Property(e => e.Secret).IsEncrypted();
421+
});
422+
}
423+
}
424+
425+
private sealed class CacheWarningEntity
426+
{
427+
public Guid Id { get; set; }
428+
public string Secret { get; set; } = string.Empty;
429+
}
430+
371431
private static async Task CreatePfxAsync(string path, string password)
372432
{
373433
Directory.CreateDirectory(Path.GetDirectoryName(path)!);

tests/EfCore.EncryptedProperties.Tests/Integration/RotationTests.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using EfCore.EncryptedProperties.Extensions;
44
using EfCore.EncryptedProperties.KeyManagement;
55
using Microsoft.EntityFrameworkCore;
6-
using Microsoft.EntityFrameworkCore.Diagnostics;
76
using Microsoft.EntityFrameworkCore.Storage;
87
using Microsoft.Extensions.DependencyInjection;
98

@@ -182,8 +181,6 @@ private static ServiceProvider CreateProvider(
182181
services.AddDbContext<TestDbContext>((sp, builder) =>
183182
{
184183
builder.UseInMemoryDatabase(dbName);
185-
builder.ConfigureWarnings(warnings =>
186-
warnings.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning));
187184
builder.UseEncryptedProperties(sp);
188185
});
189186
return services.BuildServiceProvider();
@@ -213,8 +210,6 @@ private static ServiceProvider CreateFileKeyRingProvider(
213210
services.AddDbContext<TestDbContext>((sp, builder) =>
214211
{
215212
builder.UseInMemoryDatabase(dbName, dbRoot);
216-
builder.ConfigureWarnings(warnings =>
217-
warnings.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning));
218213
builder.UseEncryptedProperties(sp);
219214
});
220215
return services.BuildServiceProvider();

0 commit comments

Comments
 (0)