Skip to content

Commit 436bbda

Browse files
authored
Merge pull request #92 from MrDave1999/feature/issue_73
Added support for binding a configuration class with the keys of the .env file
2 parents 5d92b0e + 21cb63e commit 436bbda

File tree

8 files changed

+290
-0
lines changed

8 files changed

+290
-0
lines changed

src/Binder/BinderException.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace DotEnv.Core
5+
{
6+
/// <summary>
7+
/// The exception that is thrown when the binder encounters one or more errors.
8+
/// </summary>
9+
public class BinderException : Exception
10+
{
11+
/// <summary>
12+
/// Initializes a new instance of the <see cref="BinderException" /> class.
13+
/// </summary>
14+
public BinderException()
15+
{
16+
17+
}
18+
19+
/// <summary>
20+
/// Initializes a new instance of the <see cref="BinderException" /> class with the a specified error message.
21+
/// </summary>
22+
/// <param name="message">The message that describes the error.</param>
23+
public BinderException(string message) : base(message)
24+
{
25+
}
26+
}
27+
}

src/Binder/EnvBinder.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reflection;
4+
using static DotEnv.Core.ExceptionMessages;
5+
6+
namespace DotEnv.Core
7+
{
8+
/// <inheritdoc cref="IEnvBinder" />
9+
public class EnvBinder : IEnvBinder
10+
{
11+
/// <summary>
12+
/// Allows access to the configuration options for the binder.
13+
/// </summary>
14+
private readonly EnvBinderOptions _configuration = new();
15+
16+
/// <summary>
17+
/// Allows access to the errors container of the binder.
18+
/// </summary>
19+
private readonly EnvValidationResult _validationResult = new();
20+
21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="EnvBinder" /> class.
23+
/// </summary>
24+
public EnvBinder()
25+
{
26+
27+
}
28+
29+
/// <summary>
30+
/// Initializes a new instance of the <see cref="EnvBinder" /> class with environment variables provider.
31+
/// </summary>
32+
/// <param name="provider">The environment variables provider.</param>
33+
public EnvBinder(IEnvironmentVariablesProvider provider)
34+
{
35+
_configuration.EnvVars = provider;
36+
}
37+
38+
/// <inheritdoc />
39+
public TSettings Bind<TSettings>() where TSettings : new()
40+
=> Bind<TSettings>(out _);
41+
42+
/// <inheritdoc />
43+
public TSettings Bind<TSettings>(out EnvValidationResult result) where TSettings : new()
44+
{
45+
var settings = new TSettings();
46+
var type = typeof(TSettings);
47+
result = _validationResult;
48+
foreach (PropertyInfo property in type.GetProperties())
49+
{
50+
var envKeyAttribute = (EnvKeyAttribute)Attribute.GetCustomAttribute(property, typeof(EnvKeyAttribute));
51+
var variableName = envKeyAttribute is not null ? envKeyAttribute.Name : property.Name;
52+
var retrievedValue = _configuration.EnvVars[variableName];
53+
54+
if (retrievedValue is null)
55+
{
56+
var errorMsg = envKeyAttribute is not null ? string.Format(KeyAssignedToPropertyIsNotSet, type.Name, property.Name, envKeyAttribute.Name)
57+
: string.Format(PropertyDoesNotMatchConfigKeyMessage, property.Name);
58+
_validationResult.Add(errorMsg);
59+
continue;
60+
}
61+
62+
try
63+
{
64+
property.SetValue(settings, Convert.ChangeType(retrievedValue, property.PropertyType));
65+
}
66+
catch (FormatException)
67+
{
68+
_validationResult.Add(errorMsg: string.Format(FailedConvertConfigurationValueMessage, variableName, property.PropertyType.Name, retrievedValue, property.PropertyType.Name));
69+
}
70+
}
71+
72+
if(_validationResult.HasError())
73+
throw new BinderException(message: _validationResult.ErrorMessages);
74+
75+
return settings;
76+
}
77+
}
78+
}

src/Binder/EnvBinderOptions.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace DotEnv.Core
5+
{
6+
/// <summary>
7+
/// Represents the options for configuring various behaviors of the binder.
8+
/// </summary>
9+
public class EnvBinderOptions
10+
{
11+
/// <summary>
12+
/// Gets or sets the environment variables provider.
13+
/// </summary>
14+
public IEnvironmentVariablesProvider EnvVars { get; set; } = new DefaultEnvironmentProvider();
15+
}
16+
}

src/Binder/EnvKeyAttribute.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace DotEnv.Core
5+
{
6+
/// <summary>
7+
/// Represents the key of a .env file that is assigned to a property.
8+
/// </summary>
9+
public class EnvKeyAttribute : Attribute
10+
{
11+
/// <summary>
12+
/// Gets the name of the key the property is mapped to.
13+
/// </summary>
14+
public string Name { get; }
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="EnvKeyAttribute" /> class.
18+
/// </summary>
19+
public EnvKeyAttribute()
20+
{
21+
22+
}
23+
/// <summary>
24+
/// Initializes a new instance of the <see cref="EnvKeyAttribute" /> class with the name of the key.
25+
/// </summary>
26+
/// <param name="name">The name of the key the property is mapped to.</param>
27+
public EnvKeyAttribute(string name)
28+
{
29+
Name = name;
30+
}
31+
}
32+
}

src/Binder/IEnvBinder.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace DotEnv.Core
5+
{
6+
/// <summary>
7+
/// Allows binding strongly typed objects to configuration values.
8+
/// </summary>
9+
public interface IEnvBinder
10+
{
11+
/// <param name="result">The result contains the errors found by the binder.</param>
12+
/// <inheritdoc cref="Bind()" />
13+
TSettings Bind<TSettings>(out EnvValidationResult result) where TSettings : new();
14+
15+
/// <summary>
16+
/// Binds the instance of the environment variables provider to a new instance of type TSettings.
17+
/// </summary>
18+
/// <typeparam name="TSettings">The type of the new instance to bind.</typeparam>
19+
/// <exception cref="BinderException">
20+
/// If the binder encounters one or more errors.
21+
/// </exception>
22+
/// <returns>The new instance of TSettings.</returns>
23+
TSettings Bind<TSettings>() where TSettings : new();
24+
}
25+
}

src/Constants/ExceptionMessages.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,8 @@ public class ExceptionMessages
2121
public const string RequiredKeysNotPresentMessage = "'{0}' is a key required by the application.";
2222
public const string LengthOfParamsListIsZeroMessage = "The length of the params list is zero.";
2323
public const string EncodingNotFoundMessage = "'{0}' is not a supported encoding name. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method.";
24+
public const string PropertyDoesNotMatchConfigKeyMessage = "The '{0}' property does not match any configuration key.";
25+
public const string KeyAssignedToPropertyIsNotSet = "Could not set the value in the '{0}.{1}' property because the '{2}' key is not set.";
26+
public const string FailedConvertConfigurationValueMessage = "Failed to convert configuration value of '{0}' to type '{1}'. '{2}' is not a valid value for {3}.";
2427
}
2528
}

tests/Binder/AppSettings.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
namespace DotEnv.Core.Tests.Binder;
2+
3+
public class AppSettings
4+
{
5+
[EnvKey("BIND_JWT_SECRET")]
6+
public string JwtSecret { get; set; }
7+
8+
[EnvKey("BIND_TOKEN_ID")]
9+
public string TokenId { get; set; }
10+
11+
[EnvKey("BIND_RACE_TIME")]
12+
public int RaceTime { get; set; }
13+
14+
public string BindSecretKey { get; set; }
15+
public string BindJwtSecret { get; set; }
16+
}
17+
18+
public class SettingsExample1
19+
{
20+
public string SecretKey { get; set; }
21+
}
22+
23+
public class SettingsExample2
24+
{
25+
[EnvKey("SECRET_KEY")]
26+
public string SecretKey { get; set; }
27+
}
28+
29+
public class SettingsExample3
30+
{
31+
[EnvKey("BIND_WEATHER_ID")]
32+
public int WeatherId { get; set; }
33+
}

tests/Binder/EnvBinderTests.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
namespace DotEnv.Core.Tests.Binder;
2+
3+
[TestClass]
4+
public class EnvBinderTests
5+
{
6+
[TestMethod]
7+
public void Bind_WhenPropertiesAreLinkedToTheDefaultProviderInstance_ShouldReturnsSettingsInstance()
8+
{
9+
SetEnvironmentVariable("BIND_JWT_SECRET", "12example");
10+
SetEnvironmentVariable("BIND_TOKEN_ID", "e32d");
11+
SetEnvironmentVariable("BIND_RACE_TIME", "23");
12+
SetEnvironmentVariable("BindSecretKey", "12example");
13+
SetEnvironmentVariable("BindJwtSecret", "secret123");
14+
15+
var settings = new EnvBinder().Bind<AppSettings>();
16+
17+
Assert.AreEqual(expected: "12example", actual: settings.JwtSecret);
18+
Assert.AreEqual(expected: "e32d", actual: settings.TokenId);
19+
Assert.AreEqual(expected: 23, actual: settings.RaceTime);
20+
Assert.AreEqual(expected: "12example", actual: settings.BindSecretKey);
21+
Assert.AreEqual(expected: "secret123", actual: settings.BindJwtSecret);
22+
}
23+
24+
[TestMethod]
25+
public void Bind_WhenPropertiesAreLinkedToTheCustomProviderInstance_ShouldReturnsSettingsInstance()
26+
{
27+
var customProvider = new CustomEnvironmentVariablesProvider();
28+
customProvider["BIND_JWT_SECRET"] = "13example";
29+
customProvider["BIND_TOKEN_ID"] = "e31d";
30+
customProvider["BIND_RACE_TIME"] = "24";
31+
customProvider["BindSecretKey"] = "13example";
32+
customProvider["BindJwtSecret"] = "secret124";
33+
34+
var settings = new EnvBinder(customProvider).Bind<AppSettings>();
35+
36+
Assert.AreEqual(expected: "13example", actual: settings.JwtSecret);
37+
Assert.AreEqual(expected: "e31d", actual: settings.TokenId);
38+
Assert.AreEqual(expected: 24, actual: settings.RaceTime);
39+
Assert.AreEqual(expected: "13example", actual: settings.BindSecretKey);
40+
Assert.AreEqual(expected: "secret124", actual: settings.BindJwtSecret);
41+
}
42+
43+
[TestMethod]
44+
public void Bind_WhenPropertyDoesNotMatchConfigurationKey_ShouldThrowBinderException()
45+
{
46+
var binder = new EnvBinder();
47+
48+
void action() => binder.Bind<SettingsExample1>();
49+
50+
var ex = Assert.ThrowsException<BinderException>(action);
51+
StringAssert.Contains(ex.Message, string.Format(PropertyDoesNotMatchConfigKeyMessage, "SecretKey"));
52+
}
53+
54+
[TestMethod]
55+
public void Bind_WhenKeyAssignedToThePropertyIsNotSet_ShouldThrowBinderException()
56+
{
57+
var binder = new EnvBinder();
58+
59+
void action() => binder.Bind<SettingsExample2>();
60+
61+
var ex = Assert.ThrowsException<BinderException>(action);
62+
StringAssert.Contains(ex.Message, string.Format(KeyAssignedToPropertyIsNotSet, "SettingsExample2", "SecretKey", "SECRET_KEY"));
63+
}
64+
65+
[TestMethod]
66+
public void Bind_WhenConfigurationValueCannotBeConvertedToAnotherDataType_ShouldThrowBinderException()
67+
{
68+
var binder = new EnvBinder();
69+
SetEnvironmentVariable("BIND_WEATHER_ID", "This is not an int");
70+
71+
void action() => binder.Bind<SettingsExample3>();
72+
73+
var ex = Assert.ThrowsException<BinderException>(action);
74+
StringAssert.Contains(ex.Message, string.Format(FailedConvertConfigurationValueMessage, "BIND_WEATHER_ID", "Int32", "This is not an int", "Int32"));
75+
}
76+
}

0 commit comments

Comments
 (0)