Creating an IConfigurationBuilder method extension to load env files

I recently needed some boilerplate code to load env files into IConfigurationBuilder.

Asp.net is notorious for having amazingly fragmented APIs, so I needed 3 static classes, simply for one method that loads the env file configuration and loads it to the configuration.

The extension has the same license as dotnet due to me lifting the code from dotnet itself.

//
// Licensed under the Apache License, Version 2.0.
// See https://github.com/aspnet/Configuration/blob/master/LICENSE.txt for license information.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using DotNetEnv;
using Microsoft.Extensions.Configuration;

namespace DroHub.ProgramExtensions {
    public static class EnvFileExtension {
        public static IConfigurationBuilder LoadFromEnvFile(this IConfigurationBuilder builder,
            string path, string prefix, LoadOptions options) {
            return builder.Add(new EnvFileConfigurationSource(path, prefix, options));
        }
    }

    public class EnvFileConfigurationSource : IConfigurationSource {
        private readonly LoadOptions _load_options;
        private readonly string _path;
        private readonly string _prefix;

        public EnvFileConfigurationSource(string path, string prefix, LoadOptions load_options) {
            _load_options = load_options;
            _path = path;
            _prefix = prefix;
        }

        public IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            return new EnvFileConfigurationProvider(_path, _prefix, _load_options);
        }
    }
    
    public class EnvFileConfigurationProvider : ConfigurationProvider{
        private readonly LoadOptions _options;
        private readonly string _path;
        private readonly string _prefix;

        public EnvFileConfigurationProvider(string path, string prefix, LoadOptions options) {
            _options = options;
            _path = path;
            _prefix = prefix;
        }

        public override void Load() {
            var env = Env.Load(_path, _options).ToDictionary();

            var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            var filtered_variables = env
                .SelectMany(NormalizeKey)
                .Where(entry => ((string)entry.Key).StartsWith(_prefix, StringComparison.OrdinalIgnoreCase));

            foreach (var envVariable in filtered_variables)
            {
                var key = ((string)envVariable.Key)[_prefix.Length..];
                data[key] = (string)envVariable.Value;
            }

            Data = data;
        }

        private static IEnumerable<DictionaryEntry> NormalizeKey(KeyValuePair<string, string> e) {
            var (key, value) = e;
            yield return new DictionaryEntry() {
                Key = NormalizeKey(key),
                Value = value
            };
        }

        private static string NormalizeKey(string key)
        {
            return key.Replace("__", ConfigurationPath.KeyDelimiter);
        }
    }
}

An ASP.net core validator to reject empty IEnumerables

This is a post on a recent problem I faced. As you may know from my previous post on Check suming axios downloaded files in jest I give very high priority to integration tests of backend/frontend communication. The story goes like:

I have a test which sets up files tracked by the backend. The test then launches a frontend action which POSTs a request with the list of files to delete. When the frontend finishes it’s thing, the test checks whether the file was indeed deleted on the backend. Turns out the frontend was saying everything was fine, but the test failed because the file was not actually deleted. The test failed successfully 🙂

To spare you the details the bug was that the frontend expected more files than the test was providing and there was an out of bound access, which in Javascript means undefined. When qs stringify finds a property with undefined value it just skips it with no error(I hate it, i mean what is the point of safe languages). This meant that my backend was receiving a POST request to delete files but the list was empty. Before the ValidationAttribute I am going to show you, this was a valid request and HTTP OK was sent to the frontend. The ValidationAttribute now makes it so there cannot be empty lists passed to the deleteFiles endpoint, and the frontend will get a notification that something went wrong.

 public class NonEmptyEnumerableValidator : ValidationAttribute {
            public override bool IsValid(object value) {
                if (!(value is IEnumerable enumerable))
                    return false;

                return enumerable.GetEnumerator().MoveNext();
            }
        }
 }
...
public async Task<IActionResult> DeleteMediaObjects([NonEmptyEnumerableValidator]IEnumerable<string> MediaIdList) { ... }

This validator makes sure the value exists, thus it is required, and needs to be a list with at least one element.

In some sources the GetEnumerator was used as a disposable but I found no evidence of that, so I am not going to be doing cargo cults. Correctly me if I am wrong though.

I also did not use enumerable.Any() because it is an internal method that is subject to be changed according to Jetbrains warnings. Fair enough.