Dotnet or .Net User-Secrets are a super useful feature to allow you not to have to manage some local variables which you might not require when you build and deploy into your cloud service. Things like a local database connection string which you’ll retrieve from the Azure Key Vault. In local you likely don’t want to be pulling secrets to connect to local development services.

Recently I had an encounter with them that gave me a ton of heartache, this is because I’m a part time developer of dotnet, mainly using fluent migrator for building a data warehouse. I’ve tried in the past sing SQL Server Developer Tools and the methods within visual Studio and 2 things come to mind. It’s always been slow and felt clunky. Also while I get the pre and post deployments I find it’s frustrating to manage.

Now back to the problem I lost a hard drive, yes an SSD failed and nope I’d never seen or heard of it before. I lost my previous copy of the user secrets I was using to build my DW locally and nope as it didn’t feel a big deal it only held a local connection string. Well as part of learning how to put something together I had copied some code and massaged it to work that I was using from my fluent migrator driver app. Not being an expert in odd behaviours of dotnet I was struggling to debug the issue. I had to learn how dotnet uses variables in Console.WriteLine. Don’t forget the place holders {0} in your string as dotnet fails to ell you that you’re doing something wrong. Once I worked that out I could work out how my indirection to hide the secret in my code had been working ad add back my new secrets

The nice thing is this code can be here, in a public or private repo and it gives nothing away about my environment, services or anything else of great value. This is the good in the use of secrets. Now with a small change to hide my computer name I can publish my appsettings.json and my secrets file as this one doesn’t hold anything of use beyond that

using System;
using System.IO;
using System.Linq;
using FluentMigrator.Runner;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Azure.Extensions.AspNetCore.Configuration.Secrets;
using Azure.Security.KeyVault.Secrets;
using Azure.Identity;




namespace DebugConsole
{
    class Program
    {
        private static IConfiguration _configuration;

        private static void Main(string[] args)
        {
            BuildConfig(args);

            var dbConnectSecret = _configuration["SecretName"];
            var sqlConnectionStr = _configuration[dbConnectSecret];
            Console.WriteLine("dbConnectSecret :  {0}", dbConnectSecret);
            Console.WriteLine("SQLConnection : {0}", sqlConnectionStr);

            var serviceProvider = CreateServices(sqlConnectionStr);

            // Put the database update into a scope to ensure
            // that all resources will be disposed.

            using (var scope = serviceProvider.CreateScope())
            {
                UpdateDatabase(scope.ServiceProvider);
            }
        }

        private static void BuildConfig(string[] args)
        {
            var configurationBuilder = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddEnvironmentVariables()
                .AddUserSecrets<Program>()
                // the <Program> stops you having to add your user secrets ID
                // meaning the code wont need updating with the key when a new dev works on it
                .AddCommandLine(args);


            var builtConfig = configurationBuilder.Build();
            
            //TODO Debugging
            var currentDir = Directory.GetCurrentDirectory();
            Console.WriteLine("Current Dir : {0} ", currentDir );
            // foreach (var child in builtConfig.GetChildren())
            // {
            //     Console.WriteLine("Child : {0}, - {1} ",child.Key, child.Value);
            // }
            
            //Settings settings = builtConfig.GetChildren().

            var keyVaultName = builtConfig["KeyVaultName"];
            var secretName = builtConfig["SecretName"];
            //TODO Debugging
            Console.WriteLine("secretName :  {0}", secretName);
            Console.WriteLine("KeyVaultName: {0} ", keyVaultName);

            if ( keyVaultName is not ("keyvaultname" or "KeyVaultName") )
            {
                if (!string.IsNullOrEmpty(keyVaultName) )
                {
                    var secretClient = new SecretClient(new Uri($"https://{keyVaultName}.vault.azure.net/"),
                        new DefaultAzureCredential());
                    configurationBuilder.AddAzureKeyVault(secretClient, new KeyVaultSecretManager());
                    var secret = secretClient.GetSecret(secretName);
                    Console.WriteLine("Secret Name : ", secret);
                    //var conString = config.GetConnectionString("DefaultDB");
                    
                }
            }

            _configuration = configurationBuilder.Build();
            
            //TODO Debugging
            // var debugView = builtConfig.GetDebugView();
            // var keys = _configuration.GetChildren();
            // foreach (var element  in keys)
            // {
            //     Console.WriteLine("Keys : {0}, - {1}", element.Key, element.Value);
            // }
            //
            // Console.WriteLine(" Debug Values : {0}", debugView);
        }

        /// <summary>
        /// Configure the dependency injection services
        /// </summary>
        private static IServiceProvider CreateServices(string sqlConnectionString)
        {
            return new ServiceCollection()
                // Add common FluentMigrator services
                
                .AddFluentMigratorCore()
                .ConfigureRunner(rb => rb
                    // Add SQLServer support to FluentMigrator
                    .AddSqlServer2016()
                    // Set the connection string
                    .WithGlobalConnectionString(sqlConnectionString)
                    // Define the assembly containing the migrations
                    .ScanIn(typeof(Scripts).Assembly).For.Migrations())
                // Enable logging to console in the FluentMigrator way
                .AddLogging(lb => lb.AddFluentMigratorConsole())
                // Build the service provider
                .BuildServiceProvider(false);
            
        }

        /// <summary>
        /// Update the database
        /// </summary>
        private static void UpdateDatabase(IServiceProvider serviceProvider)
        {
            // Instantiate the runner
            var runner = serviceProvider.GetRequiredService<IMigrationRunner>();

            // Execute the migrations
            runner.MigrateUp();
        }
    }
}
{
    "exclude" : [
        
    ],
    "SecretName" : "LocalDBConnect",
    "ConnectionStrings" : {
        "Default" : ""
    },
    "Logging": {
        "LogLevel": {
            "Default": "Debug",
            "System": "Information",
            "Microsoft": "Information"
        }
    }
}
{
  "KeyVaultName": "keyvaultname",
  "LocalDBConnect": "Data Source=DESKTOP-HK75LG3R;Initial Catalog=DwTest-01;Integrated Security=True"
}

I’ve left all my debugging in this one with the Console.WriteLine commands which will show you how to look inside the IConfiguration values. Maybe that will help another beginner or data person who needs to understand and use fluentmigrator

See ya round

Peter

Leave a Reply

Your email address will not be published. Required fields are marked *

Exit mobile version