15

When I create .net core web applications, I use the secret manager during testing. I am generally able to create a new web project (mvc and web api), right click on the project and select "manage user secrets". This opens a json file where I add the secrets. I then use this in my startup.cs something like this:

services.AddDbContext<ApplicationDbContext>(options =>
    options.UseMySql(Configuration["connectionString"]));

The website works fine with this and connects well to the database. However when I try using ef core migration commands such as add-migration, they don't seem to be able to access the connection string from the secret manager. I get the error saying "connection string can't be null". The error is gone when I hard code Configuration["connectionString"] with the actual string. I have checked online and checked the .csproj file, they already contain the following lines:

<UserSecretsId>My app name</UserSecretsId>

And later:

<ItemGroup>
<DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.1" />
<DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />

Is there anything I need to add so the migrations can access the connection string?

Update

I only have one constructor in the context class:

public ApplicationDBContext(DbContextOptions<ApplicationDBContext> options) : base(options)
{
}
Neville Nazerane
  • 6,622
  • 3
  • 46
  • 79

4 Answers4

13

I am currently coming across this exact problem as well. I have come up with a solution that works for now, but one may consider messy at best.

I have created a Configuration Class that provides the Configuration Interface when requested:

public static class Configuration
{
    public static IConfiguration GetConfiguration()
    {
        return new ConfigurationBuilder()
            .AddJsonFile("appsettings.json", true, true)
            .AddUserSecrets<Startup>()
            .AddEnvironmentVariables()
            .Build();
    }
}

In the Migration, you can then get the Configuration File and access its UserSecrets like this:

protected override void Up(MigrationBuilder migrationBuilder)
{
    var conf = Configuration.GetConfiguration();
    var secret = conf["Secret"];
}

I have tested creating a SQL Script with these User Secrets, and it works (you obviously wouldn't want to keep the Script laying around since it would expose the actual secret).

Update

The above config can also be set up into Program.cs class in the BuildWebHost method:

var config = new ConfigurationBuilder().AddUserSecrets<Startup>().Build();

return WebHost.CreateDefaultBuilder(args).UseConfiguration(config)...Build()

Or in the Startup Constructor if using that Convention

Update 2 (explanation)

It turns out this issue is because the migration scripts runs with the environment set to "Production". The secret manager is pre-set to only work in "Development" environment (for a good reason). The .AddUserSecrets<Startup>() function simply adds the secrets for all environment.

To ensure that this isn't set to your production server, there are two solutions I have noticed, one is suggested here: https://learn.microsoft.com/en-us/ef/core/miscellaneous/cli/powershell

Set env:ASPNETCORE_ENVIRONMENT before running to specify the ASP.NET Core environment.

This solution would mean there is no need to set .AddUserSecrets<Startup>() on every project created on the computer in future. However if you happen to be sharing this project across other computers, this needs to be configured on each computer.

The second solution is to set the .AddUserSecrets<Startup>() only on debug build like this:

return new ConfigurationBuilder()
    .AddJsonFile("appsettings.json", true, true)
#if DEBUG
    .AddUserSecrets<Startup>()
#endif
    .AddEnvironmentVariables()
    .Build();   

Additional Info

The Configuration Interface can be passed to Controllers in their Constructor, i.e.

private readonly IConfiguration _configuration;
public TestController(IConfiguration configuration)
{
    _configuration = configuration;
}

Thus, any Secrets and Application Setting are accessible in that Controller by accessing _configuration["secret"].

However, if you want to access Application Secrets from, for example, a Migration-File, which exists outside of the Web Application itself, you need to adhere to the original answer because there's no easy way (that I know of) to access those secrets otherwise (one use case I can think of would be seeding the Database with an Admin and a Master Password).

Jazerix
  • 4,729
  • 10
  • 39
  • 71
DeuxAlpha
  • 395
  • 4
  • 13
  • I didn't get the point of `var secret = conf["Secret"];`. I mean, if you already got the migration file, that means the migrations can already access your secret right? – Neville Nazerane Feb 19 '18 at 20:54
  • Oh, Yeah, sorry, I only tried to convey that that's how you'd access it. As in you'd have stored a secret with the key 'Secret'. – DeuxAlpha Feb 19 '18 at 23:46
  • but again, how did you get the migration file? in other words how did you run the `add-migration` command without the secret? – Neville Nazerane Feb 20 '18 at 00:17
  • Ah, I see I misunderstood your original question. I thought it was more about accessing the Secret rather than having issues accessing the Connection String itself. Two things come to mind: I assume your DbContext Constructor is set up correctly by accessing DbContextOptions, but just in case, can you post what it looks like? Another thing to look for is whether EF knows about the context you've setup. Does your DbContext show up when you run 'dotnet ef dbcontext list' ? – DeuxAlpha Feb 20 '18 at 15:09
  • I updated my constructor. The entire application works with the help of the secret manager. so the db context should know the context. regarding `dotnet ef`. The cli never recognizes it. – Neville Nazerane Feb 21 '18 at 04:43
  • 1
    Hmm, strange, especially because the CLI does not recognize the Context. Maybe you could try scaffolding an existing Database (just so the CLI would know what to reference.) Everything else looks exactly the same as mine. I only get the Error 'Value Cannot be null. Parameter name: connectionString' when I take out 'AddUserSecrets' from the ConfigurationBuilder. Have you put that statement into some kind of conditional clause where the Migration wouldn't be able to access it? – DeuxAlpha Feb 21 '18 at 14:49
  • well scaffolding would hardcode the string into the constructor. and yep once the string is in the code would work. then there is a link to a documentation that i had actually used to set up this code in the first place. so i'll just be back where i started – Neville Nazerane Feb 21 '18 at 18:50
  • Pardon my continuous confusion about the issue, but don't you only hard-code the way you access the Secret, not the secret itself? I.e. when you, as you phrased in your original question, hard-code Configuration["connectionString"], anyone else who is testing with you can decide in their Secret File what that Secret would be themselves (since you don't share that). They'd only have to assign it the key 'connectionString', the actual Value is up to them. Sorry, once again, if I'm not following. – DeuxAlpha Feb 21 '18 at 20:04
  • by the way your `AddUserSecrets` did help me resolve the issue. i have marked your answer. – Neville Nazerane Feb 21 '18 at 22:31
  • to answer your question, what i meant was, when you scaffold, the generated classes will have the connection string hard coded in the database. you can check this: https://learn.microsoft.com/en-us/ef/core/get-started/aspnetcore/existing-db . I can manually move it to the secrets based on the instructions they provide in the comments, but that will get us back to the code in my question – Neville Nazerane Feb 21 '18 at 22:36
  • 1
    Glad to be of service! – DeuxAlpha Feb 21 '18 at 22:44
  • 1
    yeah thanks. by the way all configurations are normally handled in the startup.cs class. if you want to access it else where it is recommended you use services via dependency injection – Neville Nazerane Feb 22 '18 at 03:59
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/165653/discussion-between-deuxalpha-and-neville-nazerane). – DeuxAlpha Feb 22 '18 at 14:59
6

To use migrations in NetCore with user secrets we can also set a class (SqlContextFactory) to create its own instance of the SqlContext using a specified config builder. This way we do not have to create some kind of workaround in our Program or Startup classes. In the below example SqlContext is an implementation of DbContext/IdentityDbContext.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;

public class SqlContextFactory : IDesignTimeDbContextFactory<SqlContext>
{
    public SqlContext CreateDbContext(string[] args)
    {
        var config = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json", optional: false)
            .AddUserSecrets<Startup>()
            .AddEnvironmentVariables()
            .Build();

        var builder = new DbContextOptionsBuilder<SqlContext>();
        builder.UseSqlServer(config.GetConnectionString("DefaultConnection"));
        return new SqlContext(builder.Options);
    }
}
Denis Rozhnev
  • 2,547
  • 1
  • 14
  • 15
Bob Meijwaard
  • 388
  • 9
  • 20
  • I didn't get the point of this. You are simply doing the same "some kind of work around" on a whole new class. Meaning you are just having additional code. Moreover, the code in program.cs would be required either ways for future use. – Neville Nazerane Jul 18 '18 at 18:17
  • you can look into this for a simplified version of the solution: https://stackoverflow.com/questions/50104872/asp-net-core-app-with-user-secrets-fails-on-macos-command-line-only/50111099#50111099 – Neville Nazerane Jul 18 '18 at 18:17
  • This looks like a pretty sleek suggestion to me, unless there are better ways now? This class is needed for creating migrations in the first place, so why not make it more useful and do the other bit and save clutter in the startup files. – melwil Feb 06 '19 at 18:02
3

Since I have noticed a lot of people running into this confusion, I am writing a simplified version of this resolution.

The Problem/Confusion

The secret manager in .net core is designed to work only in the Development environment. When running your app, your launchSettings.json file ensures that your ASPNETCORE_ENVIRONMENT variable is set to "Development". However, when you run EF migrations it doesn't use this file. As a result, when you run migrations, your web app does not run on the Development environment and thus no access to the secret manager. This often causes confusion as to why EF migrations can't use the secret manager.

The Resolution

Make sure your environment variable "ASPNETCORE_ENVIRONMENT" is set to "Development" in your computer.

Neville Nazerane
  • 6,622
  • 3
  • 46
  • 79
1

The way of using .AddUserSecrets<Startup>() will make a circular reference if we having our DbContext in a separate class library and using DesignTimeFactory

The clean way of doing that is:

public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
{
    public AppDbContext CreateDbContext(string[] args)
    {
        var configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
#if DEBUG
                .AddJsonFile(@Directory.GetCurrentDirectory() + 
                            "{project path}/appsettings.Development.json", 
                    optional: true, reloadOnChange: true)
#else
                .AddJsonFile(@Directory.GetCurrentDirectory() + 
                            "{startup project path}/appsettings.json",
                    optional: true, reloadOnChange: true)
#endif
                .AddEnvironmentVariables()
                .Build();
    
    
        var connectionString = configuration.GetConnectionString("DefaultConnection");

        var builder = new DbContextOptionsBuilder<AppDbContext>();

        Console.WriteLine(connectionString);
        builder.UseSqlServer(connectionString);
        return new AppDbContext(builder.Options);
    }
}

The Explanation:
Secret Manager is meant to be in the development time only, so this will not affect the migration in case if you have it in a pipeline in QA or Production stages, so to fix that we will use the dev connection string which exists in appsettings.Development.json during the #if Debug.

The benefit of using this way is to decouple referencing the Web project Startup class while using class library as your Data infrastructure.

Marzouk
  • 2,650
  • 3
  • 25
  • 56