1

I have earlier achieved this .net 3.1. But it couldn't be possible with .Net 6 because of startup.cs removed.

I have registered a few services,

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var appSettings = builder.Configuration.GetSection("AppSettings").Get<AppSettings>();

  
builder.Services.AddScoped<IEncryption, Encryption>();

//Here I need to get the IEncryption Service, and call the method in this service to encrypt/decrypt the connection string to pass to DBContext Service.

builder.Services.AddDbContext<CatalogDbContext>(options => options.UseNpgsql(
                    appSettings.ConnectionString));

var app = builder.Build();

Earlier in .NET 3.1, I used BuildServicProvider() to get the Encryption service, and call the methods in that service to do the required logic then got the proper connection string I wanted that would be passed to the DBContext service on the next line.

Now, .NET 6/7 is forced to use the services only after app = builder.Build(); so, I can't register the DBCOntext after the build() method.

How can I solve this case? Any recommended approach to do this in .NET 6/7?

Karthik
  • 729
  • 2
  • 10
  • 21
  • Encryption is the job of the configuration providers, not of `AddDbContext`. It's possible to use encrypted configuration settings in all .NET Core versions, including .NET Core 3.1 – Panagiotis Kanavos Jan 30 '23 at 08:35
  • Where do you store the connection strings and why do you want to encrypt them manually? It matters. First of all, you aren't restricted to *one* settings file. You can have several, and some of them can easily be encrypted at the file system level using, eg NTFS Encryption. You can use an encrypted provider like Hashicorp, Azure Key Vault or the equivalent services from AWS, Google. Or you can create your own config provider that decrypts one or more sections – Panagiotis Kanavos Jan 30 '23 at 08:39
  • @PanagiotisKanavos: I have my encrypted connection strings in the appsettings.json file. So, I need to decrypt the text using my own service before applying it to the DBContext service. – Karthik Jan 30 '23 at 09:11
  • I showed how to do this. You have access to a ServiceProvider instance in `AddDbContext`. It's still *not* a good idea to mix all settings in the same file though. That's why `appsettings.Production.json` exists in the first place. – Panagiotis Kanavos Jan 30 '23 at 09:15
  • Im keeping separate app settings file for production and development case. In both cases, I need to do decryption logic on the connection string text present in the app setting file. ServiceProvider instance in AddDbContext does nothing here. – Karthik Jan 30 '23 at 09:42

3 Answers3

0

You still can useStartup.cs in .net 6

var builder = WebApplication.CreateBuilder(args);
var startup = new Startup(builder.Configuration);
startup.ConfigureServices(builder.Services); // calling ConfigureServices method
var app = builder.Build();
startup.Configure(app, builder.Environment); // calling Configure method

And then you can use ConfigureServices and Configure methods to register your services before building.

Talkhak1313
  • 301
  • 2
  • 11
0

Important Caveat: In general, my suggestion below is a bad practice Do not call BuildServiceProvider

Why is bad? Calling BuildServiceProvider from application code results in more than one copy of singleton services being created which might result in incorrect application behavior.

Justification: I think it is safe to call BuildServiceProvider as long as you haven't registered any singletons before calling it. Admittedly not ideal, but it should work.

You can still callBuildServiceProvider() in .Net6:

builder.Services.AddScoped<IEncryption, Encryption>();
// create service provider
var provider = builder.Services.BuildServiceProvider();
var encryption = scope.ServiceProvider.GetService<IEncryptionService>();
// use service here

or alternatively

builder.Services.AddScoped<IEncryption, Encryption>();
var provider = builder.Services.BuildServiceProvider();
using (var scope = provider.CreateScope()) {
   var encryption = scope.ServiceProvider.GetService<IEncryptionService>();
   // use service here
}

Alternative: You can still use the classic startup structure in .Net6/7. We upgraded our .Net3.1 projects to .Net6 without having to rewrite/restructure the Startup()

djf
  • 6,592
  • 6
  • 44
  • 62
  • Thanks @djf. I already tried your suggestions, but both haven't helped me. It says the following error, "Calling "BuildServiceProvider" from application code results in an additional copy of singleton services being created. Consider alternatives such as dependency injecting services as parameters to 'Configure'. – Karthik Jan 30 '23 at 09:31
  • @Karthik You are correct. I've expanded my answer and added some caveats – djf Jan 30 '23 at 09:48
0

You didn't need to use BuildServiceProvider in .NET Core 3.1 either. AddDbContext has an overload that provides access to an IServiceProvider instance :

builder.Services.AddDbContext<CatalogDbContext>((services,options) =>{ 
    var myOwnDecrypter=services.GetRequiredService<IMyOwnDecrypter>();
    var cns=myOwnDecrypter.Decrypt(appSettings.ConnectionString,key);
    options.UseNpgsql(cns);
});

or, if you use the ASP.NET Core Data Protection package :

builder.Services.AddDataProtection();
...
builder.Services.AddDbContext<CatalogDbContext>((services,options) =>{ 
    var protector = services.GetDataProtector("Contoso.Example.v2");
    var cns=protector.Unprotect(appSettings.ConnectionString);
    options.UseNpgsql(cns);
});

or, if IConfiguration.GetConnectionString is used :

builder.Services.AddDataProtection();
...
builder.Services.AddDbContext<CatalogDbContext>((services,options) =>{ 
    var conn_string=services.GetService<IConfiguration>()
                            .GetConnectionString("MyConnectionString");
    var protector = services.GetDataProtector("Contoso.Example.v2");
    var cns=protector.Unprotect(conn_string);
    options.UseNpgsql(cns);
});

That said, it's the configuration provider's job to decrypt encrypted settings, not the service/context's. ASP.NET Core's configuration allows using multiple different configuration sources in the same host, not just a single settings file. There's nothing special about appsettings.json. That's just the default settings file name.

  • You can add another settings file with sensitive contents with AddJsonSettings. That file could use the file system's encryption, eg NTFS Encryption, to ensure it's only readable by the web app account
  • You can read settings from a key management service, like Hashicorp, Azure Key Vault, Amazon Key Management etc.
  • You can create your own provider that decrypts its input. The answers to this SO questino show how to do this and one of them inherits from JsonConfigurationProvider directly.
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • Thanks, In.NET6.0, the provided first solution doesn't work. The line "var cns=myCustomDecrypter.Decrypt(appSettings.ConnectionString,key);" does nothing. We couldn't use the registered services before the app was built. – Karthik Jan 30 '23 at 09:39
  • @Karthik that's where you put *your* code. You didn't explain how you encrypt or decrypt settings so it's impossible to give a specific answer – Panagiotis Kanavos Jan 30 '23 at 09:47
  • I have just added my own class Encryption which has a decrypt method it does AES decryption logic. I have already set up my code. But it is not calling from the line mentioned. – Karthik Jan 30 '23 at 10:05
  • What does `it is not calling from the line mentioned` mean? Does this throw an exception? Does `services.GetRequiredService()` because the service isn't registered? Is `AddDbContext` not called at all? Do you get a compilation error? `AddDbContext` already provides access to `IServiceProvider`, that's not debatable – Panagiotis Kanavos Jan 30 '23 at 10:21
  • It is executed without exception, however, I can't see the decrypt method ran at this step. The empty result is coming to the _decryptedText var. Note: I had a decrypt method in IEncryption, which return some text always. builder.Services.AddDbContext((ser, op) => { var mySer = ser.GetService(); _decryptedText = myService?.Decrypt("encrypted_text"); op.UseNpgsql(_decryptedConnectionString); }); – Karthik Jan 30 '23 at 11:36
  • `myService?.Decrypt` will return `null` if `IEncryption` isn't registered. That's why I used `GetRequiredService` instead of `GetService`. That's why I asked if the service is registered too, or if the error happens for singleton or transient services. – Panagiotis Kanavos Jan 30 '23 at 14:15
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/251510/discussion-between-karthik-and-panagiotis-kanavos). – Karthik Jan 31 '23 at 04:42
  • Thanks Panagiotis, I tried with GerRequiredService instead of GetService. But getting empty string only. Here I have already registered the IEncryption Serivce too above the AddDbContext service. builder.Services.AddScoped(serviceProvider => { var secretKey = appSettings?.AESEncryptionKey ?? string.Empty; return new Encryption(secretKey); }); – Karthik Feb 01 '23 at 04:21
  • That's not what the question asks in the first place. That's a different problem, unrelated to `IServiceProvider`. It means `appSettings` wasn't loaded. The very fact you used `?.AESEncryptionKey` means `appSettings` is null and `builder.Configuration.GetSection("AppSettings").Get();` failed to load it. – Panagiotis Kanavos Feb 01 '23 at 08:20
  • I can see appSettings section is loaded, and see the values from the appsettings file using the line builder.Configuration.GetSection("AppSettings").Get();. Also AESEncryptioinKey has actual value. I check and confirm while debugging. I can understand your actual point that you said IEncryption isn't registered. But I registered this service at first place. – Karthik Feb 01 '23 at 09:29
  • Your own code and your own comments say the opposite. `getting empty string only` and `var secretKey = appSettings?.AESEncryptionKey ?? string.Empty;`. That code explicitly returns an empty string if `appSettings` or `AESEncryptionKey` are missing. `myService?.Decrypt` means that `myService` can be null, because it wasn't registered. If you added `?.` because `.GetService()` returns `IEncryption?`, change the call to `GetRequiredService`. That returns and `IEncryption`. The same in the other calls. If the setting *isn't* supposed to be null, don't use `?.`. – Panagiotis Kanavos Feb 01 '23 at 09:41
  • Thanks for the guidance, as you said the problem is with IEncryption service registration here. However, We couldn't use GetRequieredService to call scoped one directly. Instead I tried to create scope using IServiceScopeFactory, and it works. using var scope = services?.GetService()?.CreateScope(); var service = scope?.ServiceProvider.GetRequiredService(); – Karthik Feb 02 '23 at 04:36