Implement a human readable configuration with YAML in ASP.NET Core

Sep 12, 2016     Viewed 7571 times    2 Comments
Posted in #Configuration  #YAML 

YAML (YAML Ain’t Markup Language) is a human friendly data serialization standard for all programming languages. YAML is a pretty common configuration language in the Ruby world, because of it's nature it is a perfect fit for human readable configuration files

Why YAML not XML or JSON?

Before I judge let us see the following appvoyer.yml as example:

init:
  - git config --global core.autocrlf true
branches:
  only:
    - master
    - release
    - dev
    - /^(.*\/)?ci-.*$/
build_script:
  - build.cmd --quiet verify
clone_depth: 1
test: off
deploy: off

As you can see above, this too small in contrast to XML which needs opening and closing tags for almost the elements in the file, also it more readable than JSON which has tons of opening and closing brackets {{{...}}}. In other hand YAML configuration is very useful, because it's a human readable

YAML Configuration sipport in ASP.NET Core

AFAIK ASP.NET Core till now supports three file formats out of the box: INI, JSON and XML, while YAML is not supported yet. So in this post I will try to guide you to implement a YAML configuration provider.

Let us start to implement the YamlConfigurationProvider which is responsible for loading and parsing the YAML file.

public class YamlConfigurationProvider : FileConfigurationProvider
{
    private Dictionary _data;

    public YamlConfigurationProvider(YamlConfigurationSource source) : base(source) { }

    public override void Load(Stream stream)
    {
        _data = new Dictionary(StringComparer.OrdinalIgnoreCase);

        var yaml = new YamlStream();

        yaml.Load(new StreamReader(stream));

        var mapping = (YamlMappingNode)yaml.Documents[0].RootNode;

        foreach (var entry in mapping.Children)
        {
            if (entry.Value is YamlScalarNode)
            {
                var key = ((YamlScalarNode)entry.Key).Value;
                var value = ((YamlScalarNode)entry.Value).Value;

                _data[key] = value;
            }

            if (entry.Value is YamlMappingNode)
                Traverse((YamlMappingNode)entry.Value, ((YamlScalarNode)entry.Key).Value);
        }

        Data = _data;
    }
        
    private void Traverse(YamlMappingNode node, string prefix)
    {
        foreach (var item in node.Children)
        {
            if (item.Value is YamlScalarNode)
            {
                var key = string.Concat(prefix, ConfigurationPath.KeyDelimiter, item.Key);
                    
                _data[key] = item.Value.ToString();
            }
                
            if (item.Value is YamlMappingNode)
            {
                prefix+= string.Concat(ConfigurationPath.KeyDelimiter, ((YamlScalarNode)item.Key).Value);
                Traverse((YamlMappingNode)item.Value, prefix);
            }
        }
    }
}

After that YamlConfigurationSource which represents the actual YAML file as IConfigurationSource.

public class YamlConfigurationSource : FileConfigurationSource
{
    public override IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        FileProvider = FileProvider ?? builder.GetFileProvider();
        return new YamlConfigurationProvider(this);
    }
}

Then YamlConfigurationExtensions which is nothing but a punch of extension methods for adding YamlConfigurationProvider.

public static class YamlConfigurationExtensions
{
    public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, string path)
    {
        return AddYamlFile(builder, provider: null, path: path, optional: false, reloadOnChange: false);
    }

    public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, string path, bool optional)
    {
        return AddYamlFile(builder, provider: null, path: path, optional: optional, reloadOnChange: false);
    }

    public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange)
    {
        return AddYamlFile(builder, provider: null, path: path, optional: optional, reloadOnChange: reloadOnChange);
    }

    public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange)
    {
        if (builder == null)
        {
            throw new ArgumentNullException(nameof(builder));
        }
            
        if (string.IsNullOrEmpty(path))
        {
            throw new ArgumentException("InvalidFilePath", nameof(path));
        }

        var source = new YamlConfigurationSource
        {
            FileProvider = provider,
            Path = path,
            Optional = optional,
            ReloadOnChange = reloadOnChange
        };

        builder.Add(source);
        return builder;
}

That is it!! we saw how YAML configuration is simple to implement in ASP.NET Core, of course with the help of YamlDotNet.

You can download the source code for this post from my YamlConfiguration repository.

Hint: my implementation is limited to support YamlScalarNode and YamlMappingNode, so SequenceMappingNode will be supported soon.

Twitter Facebook Google + LinkedIn


2 Comments

Andrew Lock (9/14/2016 11:05:11 AM)

Nice post, I also wrote about the same thing, and used pretty much the same approach as far as I can see, including YamlDotNet:)

If you're interested, the post is here: http://andrewlock.net/creating-a-custom-iconfigurationprovider-in-asp-net-core-to-parse-yaml/ and the source code / NuGet package is here: https://github.com/andrewlock/NetEscapades.Configuration.

Hisham Bin Ateya (9/17/2016 7:44:50 PM)

@Andrew Lock I was planning to write a YAML parser from scratch, but it's time consuming and I don't wanna re-invent the wheel :), I find the YamlDotNet make it easier for me to deal with YAML.
Thanks for sharing your post, I see Orchard follow the same approach too :)


Leave a Comment