Skip to content

Commit

Permalink
Add Initial Implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
lawrence-laz committed Oct 13, 2020
1 parent 4b30076 commit 8b1d1e2
Show file tree
Hide file tree
Showing 13 changed files with 487 additions and 0 deletions.
56 changes: 56 additions & 0 deletions Extensions.Configuration.Object.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30503.244
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{DE5D710E-2B98-42F1-8E4B-E16A75C6D566}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests", "test\Extensionsions.Configuration.Object.UnitTests\UnitTests.csproj", "{7653901A-5E68-46EE-853E-CFA1AA9950F9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Extensions.Configuration.Object", "src\Extensions.Configuration.Object\Extensions.Configuration.Object.csproj", "{DADDEA83-7AE9-4EF8-B826-1500FE514467}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7653901A-5E68-46EE-853E-CFA1AA9950F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7653901A-5E68-46EE-853E-CFA1AA9950F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7653901A-5E68-46EE-853E-CFA1AA9950F9}.Debug|x64.ActiveCfg = Debug|Any CPU
{7653901A-5E68-46EE-853E-CFA1AA9950F9}.Debug|x64.Build.0 = Debug|Any CPU
{7653901A-5E68-46EE-853E-CFA1AA9950F9}.Debug|x86.ActiveCfg = Debug|Any CPU
{7653901A-5E68-46EE-853E-CFA1AA9950F9}.Debug|x86.Build.0 = Debug|Any CPU
{7653901A-5E68-46EE-853E-CFA1AA9950F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7653901A-5E68-46EE-853E-CFA1AA9950F9}.Release|Any CPU.Build.0 = Release|Any CPU
{7653901A-5E68-46EE-853E-CFA1AA9950F9}.Release|x64.ActiveCfg = Release|Any CPU
{7653901A-5E68-46EE-853E-CFA1AA9950F9}.Release|x64.Build.0 = Release|Any CPU
{7653901A-5E68-46EE-853E-CFA1AA9950F9}.Release|x86.ActiveCfg = Release|Any CPU
{7653901A-5E68-46EE-853E-CFA1AA9950F9}.Release|x86.Build.0 = Release|Any CPU
{DADDEA83-7AE9-4EF8-B826-1500FE514467}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DADDEA83-7AE9-4EF8-B826-1500FE514467}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DADDEA83-7AE9-4EF8-B826-1500FE514467}.Debug|x64.ActiveCfg = Debug|Any CPU
{DADDEA83-7AE9-4EF8-B826-1500FE514467}.Debug|x64.Build.0 = Debug|Any CPU
{DADDEA83-7AE9-4EF8-B826-1500FE514467}.Debug|x86.ActiveCfg = Debug|Any CPU
{DADDEA83-7AE9-4EF8-B826-1500FE514467}.Debug|x86.Build.0 = Debug|Any CPU
{DADDEA83-7AE9-4EF8-B826-1500FE514467}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DADDEA83-7AE9-4EF8-B826-1500FE514467}.Release|Any CPU.Build.0 = Release|Any CPU
{DADDEA83-7AE9-4EF8-B826-1500FE514467}.Release|x64.ActiveCfg = Release|Any CPU
{DADDEA83-7AE9-4EF8-B826-1500FE514467}.Release|x64.Build.0 = Release|Any CPU
{DADDEA83-7AE9-4EF8-B826-1500FE514467}.Release|x86.ActiveCfg = Release|Any CPU
{DADDEA83-7AE9-4EF8-B826-1500FE514467}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{7653901A-5E68-46EE-853E-CFA1AA9950F9} = {DE5D710E-2B98-42F1-8E4B-E16A75C6D566}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4EC01313-2DCE-42CE-B022-1EA967A62372}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Company>Laurynas Lazauskas</Company>
<Product>Extensions.Configuration.Object</Product>
<Description>A simple way to add configuration using plain objects.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/lawrence-laz/Extensions.Configuration.Object</PackageProjectUrl>
<RepositoryUrl>https://github.com/lawrence-laz/Extensions.Configuration.Object</RepositoryUrl>
<PackageTags>configuration;options;object;extensions</PackageTags>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Copyright>Copyright 2020</Copyright>
<Version>1.0.0</Version>
<RepositoryType>git</RepositoryType>
<PackageIcon>icon.png</PackageIcon>
<PackageIconUrl />
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="3.1.8" />
</ItemGroup>

</Project>
7 changes: 7 additions & 0 deletions src/Extensions.Configuration.Object/Internal/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Extensions.Configuration.Object.Internal
{
internal static class Constants
{
public const string IssuesUrl = "https://github.com/lawrence-laz/Extensions.Configuration.Object/issues";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;

namespace Extensions.Configuration.Object.Internal
{
internal static class ReflectionExtensions
{
// Gets a sanitized name of a field. In case of anonymous objects name is unmangled, ex. "<MyField>i__Field" -> "MyField".
public static string GetName(this FieldInfo field)
{
if (field.DeclaringType.IsAnonymous())
{
var match = Regex.Match(field.Name, "<(.*)>", RegexOptions.Compiled);
if (!match.Success || match.Groups.Count < 2 || !match.Groups[1].Success || string.IsNullOrWhiteSpace(match.Groups[1].Value))
{
throw new Exception($"Unsupported field name '{field.Name}'. Please report this to {Constants.IssuesUrl}.");
}

return match.Groups[1].Value;
}

return field.Name;
}

public static bool TryGetFields(this Type type, out FieldInfo[] fields)
{
var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
fields = type.GetFields(bindingFlags);

return fields.Length > 0;
}

public static bool IsAnonymous(this Type type)
{
return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)
&& type.IsGenericType
&& (type.Name.Contains("AnonymousType") || type.Name.Contains("AnonType"))
&& (type.Name.StartsWith("<>") || type.Name.StartsWith("VB$"))
&& type.Attributes.HasFlag(TypeAttributes.NotPublic);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Microsoft.Extensions.Configuration;
using System;

namespace Extensions.Configuration.Object
{
/// <summary>
/// Extension methods for adding <see cref="ObjectConfigurationProvider"/>.
/// </summary>
public static class ObjectConfigurationExtensions
{
/// <summary>
/// Adds a <see cref="ObjectConfigurationProvider"/> to with a given configuration object.
/// </summary>
/// <param name="builder"><see cref="IConfigurationBuilder"/> to add provider to.</param>
/// <param name="configurationObject">Configuration object with configuration values.</param>
/// <returns>Same <see cref="IConfigurationBuilder"/> instance to continue configuration.</returns>
public static IConfigurationBuilder AddObject(this IConfigurationBuilder builder, object configurationObject)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}

if (configurationObject is null)
{
throw new ArgumentNullException(nameof(configurationObject));
}

return builder.Add((ObjectConfigurationSource source) => source.ConfigurationObject = configurationObject);
}
}
}
61 changes: 61 additions & 0 deletions src/Extensions.Configuration.Object/ObjectConfigurationProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Extensions.Configuration.Object.Internal;
using Microsoft.Extensions.Configuration;
using System;

namespace Extensions.Configuration.Object
{
/// <summary>
/// Loads configuration key/values from an object into a provider.
/// </summary>
public class ObjectConfigurationProvider : ConfigurationProvider
{
/// <summary>
/// Object used as a source for configuration.
/// </summary>
public object ConfigurationObject { get; set; }

/// <summary>
/// Creates an instance of <see cref="ObjectConfigurationProvider"/> using the provided configuration object.
/// </summary>
/// <param name="configurationObject">Object used as a source for configuration.</param>
public ObjectConfigurationProvider(object configurationObject)
{
ConfigurationObject = configurationObject ?? throw new ArgumentNullException(nameof(configurationObject));
}

/// <summary>
/// Recursively loads values from <see cref="ConfigurationObject"/> to this provider.
/// </summary>
public override void Load()
{
LoadRecursively(null, ConfigurationObject);
}

private void LoadRecursively(string currentKey, object section)
{
if (section is null)
{
return;
}

if (section is string || section.GetType().IsPrimitive)
{
base.Set(currentKey, section.ToString());
}
else if (section.GetType().TryGetFields(out var fields))
{
foreach (var field in fields)
{
var name = field.GetName();
var value = field.GetValue(section);

var newKey = string.IsNullOrWhiteSpace(currentKey)
? name
: $"{currentKey}:{name}";

LoadRecursively(newKey, value);
}
}
}
}
}
31 changes: 31 additions & 0 deletions src/Extensions.Configuration.Object/ObjectConfigurationSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Microsoft.Extensions.Configuration;
using System;

namespace Extensions.Configuration.Object
{
/// <summary>
/// Represents an anonymous, class or struct object as an <see cref="IConfigurationSource"/>.
/// </summary>
public class ObjectConfigurationSource : IConfigurationSource
{
/// <summary>
/// Object used as a source for configuration.
/// </summary>
public object ConfigurationObject { get; set; }

/// <summary>
/// Builds the <see cref="ObjectConfigurationProvider"/> for this source.
/// </summary>
/// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param>
/// <returns>An <see cref="ObjectConfigurationProvider"/></returns>
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
if (ConfigurationObject is null)
{
throw new ArgumentNullException($"Property {ConfigurationObject} needs to be set before calling method {nameof(Build)}.");
}

return new ObjectConfigurationProvider(ConfigurationObject);
}
}
}
Binary file added src/Extensions.Configuration.Object/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Xunit;

namespace Extensions.Configuration.Object.UnitTests
{
public class AnonymousObjectTest
{
[Fact]
public void AddObject_WithAnonymousObject_ShouldLoadFieldsIntoConfiguration()
{
var configuration = new ConfigurationBuilder()
.AddObject(new
{
Position = new
{
Title = "Editor",
Name = "Joe Smith",
Age = 33
},
MyKey = "My appsettings.json Value",
Logging = new
{
LogLevel = new
{
Default = "Information",
Microsoft = "Warning"
}
},
AllowedHosts = "*"
})
.Build();

configuration["Position:Title"].Should().Be("Editor");
configuration["Position:Name"].Should().Be("Joe Smith");
configuration["Position:Age"].Should().Be("33");
configuration["MyKey"].Should().Be("My appsettings.json Value");
configuration["Logging:LogLevel:Default"].Should().Be("Information");
configuration["Logging:LogLevel:Microsoft"].Should().Be("Warning");
configuration["AllowedHosts"].Should().Be("*");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using FluentAssertions;
using Microsoft.Extensions.Configuration;
using Xunit;

namespace Extensions.Configuration.Object.UnitTests
{
public class ClassObjectTest
{
internal class MyConfiguration
{
public PositionConfiguration Position;
public string MyKey;
public LoggingConfiguration Logging;
public string AllowedHosts;
}

internal class PositionConfiguration
{
public string Title;
public string Name;
public int Age;
}

internal class LoggingConfiguration
{
public LogLevelConfiguration LogLevel;
}

internal class LogLevelConfiguration
{
public string Default;
public string Microsoft;
}

[Fact]
public void AddObject_WithAnonymousObject_ShouldLoadFieldsIntoConfiguration()
{
var configuration = new ConfigurationBuilder()
.AddObject(new MyConfiguration
{
Position = new PositionConfiguration
{
Title = "Editor",
Name = "Joe Smith",
Age = 33
},
MyKey = "My appsettings.json Value",
Logging = new LoggingConfiguration
{
LogLevel = new LogLevelConfiguration
{
Default = "Information",
Microsoft = "Warning"
}
},
AllowedHosts = "*"
})
.Build();

configuration["Position:Title"].Should().Be("Editor");
configuration["Position:Name"].Should().Be("Joe Smith");
configuration["Position:Age"].Should().Be("33");
configuration["MyKey"].Should().Be("My appsettings.json Value");
configuration["Logging:LogLevel:Default"].Should().Be("Information");
configuration["Logging:LogLevel:Microsoft"].Should().Be("Warning");
configuration["AllowedHosts"].Should().Be("*");
}
}
}
Loading

0 comments on commit 8b1d1e2

Please sign in to comment.