Creating a DbContextFactory that gets the connection string from user-secrets

asp.net-core c# entity-framework-core

Question

Working on a DotNetCore solution with a WebApi project and a separate Data project that houses the Entity Framework implementation. We've been upgrading libraries as they come out, so we're using all of the newest Core stuff.

In the Data project, we created an ApplicationDbContextFactory in order to create migrations (needs a parameterless constructor). Due to the parameterless constructor constraint when adding a migration, you can't inject IOptions<> to easily access appsettings.json values. We ended up using a ConfigurationBuilder to pull in the WebApi's appsettings.json file(s).

We recently changed the ApplicationDbContextFactory to also pull in user-secrets. This allows each developer to use a custom connection string without having to ignore a file or remember to not commit something.

Since making this change, using dotnet ef migrations add MIGRATION_NAME works just fine in command line. However, using add-migration MIGRATION_NAME in Visual Studio's Package Manager Console now appears to be broken with the following error:

add-migration : Exception calling “Substring” with “1" argument(s): “StartIndex cannot be less than zero. Parameter name: startIndex” At line:1 char:1 + add-migration TESTING + ~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [Add-Migration], MethodInvocationException + FullyQualifiedErrorId : ArgumentOutOfRangeException,Add-Migration

I tried a few variations of the command to see if it needed the context to be specified (among other things), but nothing seemed to get around this error. It never seems to get past the constructor in ApplicationDbContextFactory.

Here's the code I'm referring to:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Models.Domain.Settings;
using System;
using System.Diagnostics;

namespace Data
{
    public class ApplicationDbContextFactory : IDbContextFactory<ApplicationDbContext>
    {
        private readonly SolutionSettings _settings;

        // In order to use 'add-migration' in Visual Studio, you have to have a parameterless constructor.
        // Otherwise you get "No parameterless constructor defined for this object." when creating a migration.
        public ApplicationDbContextFactory()
        {
        }

        public ApplicationDbContextFactory(IOptions<SolutionSettings> settings)
        {
            _settings = settings.Value;
        }

        public ApplicationDbContext Create(DbContextFactoryOptions options)
        {
            // If the IOptions signature was hit, we can just pull the dbconnection from settings
            if (_settings != null && _settings.DbConnection != null)
            {
                var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>()
                    .UseSqlServer(_settings.DbConnection, opts => {
                        opts.EnableRetryOnFailure();
                        opts.MigrationsAssembly("Data");
                    });

                return new ApplicationDbContext(optionsBuilder.Options);
            }
            else
            {
                // Otherwise, we have to get the settings manually...
                return Create(options.ContentRootPath, options.EnvironmentName);
            }
        }

        private ApplicationDbContext Create(string basePath, string environmentName)
        {
            // HACK: To pull from WebApi\appsettings.json
            basePath = basePath.Replace("Data", "WebApi");

            Console.Write($"PATH & ENV: {basePath}, {environmentName}" + Environment.NewLine);

            // Pull in the WebApi\appsettings.json files, apply user secrets
            var builder = new ConfigurationBuilder()
                .SetBasePath(basePath)
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile($"appsettings.{environmentName.ToLower()}.json", optional: true, reloadOnChange: true)
                // This needs to match the UserSecretsId value in the WebApi.csproj
                // Also added a UserSecretsId key with the same value to Data.csproj to suppress a warning
                // Adding this was the only way it would actually override values with user-secret values
                .AddUserSecrets("USER_SECRETS_ID")
                .AddEnvironmentVariables();

            var config = builder.Build();
            var connectionString = config["SolutionSettings:DbConnection"];

            Console.Write($"CONNECTION STRING: {connectionString}" + Environment.NewLine);

            return Create(connectionString);
        }

        private ApplicationDbContext Create(string connectionString)
        {
            if (string.IsNullOrEmpty(connectionString))
                throw new ArgumentException(
                    $"{nameof(connectionString)} is null or empty.",
                    nameof(connectionString));

            var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>()
                .UseSqlServer(connectionString, options => {
                    options.EnableRetryOnFailure();
                    options.MigrationsAssembly("Data");
                });

            return new ApplicationDbContext(optionsBuilder.Options);
        }
    }
}

As a side note; while troubleshooting this I added opts.EnableRetryOnFailure(); and opts.MigrationsAssembly("Data");, but I don't know that they make any difference in this context.

My questions:

  • This was initially implemented during the RC era of Core and may be a little outdated. Is there a better way to accomplish pulling user-secret values when creating migrations? Is it still a thing to use the factory like this?
  • Anyone know why we're getting that error in Visual Studio's Package Manager Console?
1
5
5/5/2017 1:53:22 PM

Popular Answer

It's been a long time since you posted it, but I've just had this error and found out the reason (even though it makes no sense)

The problem is in the line

console.Write($"CONNECTION STRING: {connectionString}" + Environment.NewLine);

If you romove the colon after CONNECTION STRING it works. I have no idea why the colon in the interpolation is causing this error

0
11/20/2018 1:37:15 PM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow