LANCommander/LANCommander.Server/Configuration/ConnectionStringBinder.cs

188 lines
6.8 KiB
C#

using System.ComponentModel;
using System.Globalization;
using System.Reflection;
namespace LANCommander.Server.Configuration;
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public sealed class ConnectionStringKeyAttribute : Attribute
{
public string Key { get; }
public ConnectionStringKeyAttribute(string key) => Key = key;
}
public static class ConnectionStringBinder
{
public static T Bind<T>(string connectionString) where T : new()
=> (T)Bind(connectionString, typeof(T));
public static object Bind(string connectionString, Type targetType)
{
if (targetType == null) throw new ArgumentNullException(nameof(targetType));
var instance = Activator.CreateInstance(targetType)
?? throw new InvalidOperationException($"Could not create {targetType}.");
var dict = Parse(connectionString);
// Build a lookup from possible keys → property
var props = targetType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.CanWrite);
var map = new Dictionary<string, PropertyInfo>(StringComparer.OrdinalIgnoreCase);
foreach (var p in props)
{
// Primary key: property name
map[p.Name] = p;
// Aliases via attribute(s)
foreach (var attr in p.GetCustomAttributes<ConnectionStringKeyAttribute>())
map[attr.Key] = p;
}
foreach (var (key, raw) in dict)
{
if (!map.TryGetValue(key, out var prop)) continue;
var converted = ConvertTo(raw, prop.PropertyType);
prop.SetValue(instance, converted);
}
return instance;
}
/// <summary>
/// Parse a connection string into a dictionary of key/value pairs.
/// Handles quoted values containing semicolons.
/// Last duplicate key wins.
/// </summary>
public static IDictionary<string, string> Parse(string connectionString)
{
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(connectionString)) return result;
int i = 0;
int n = connectionString.Length;
while (i < n)
{
// Skip leading whitespace and semicolons
while (i < n && (char.IsWhiteSpace(connectionString[i]) || connectionString[i] == ';')) i++;
if (i >= n) break;
// Read key up to '='
int keyStart = i;
while (i < n && connectionString[i] != '=') i++;
if (i >= n)
{
// No '=', ignore trailing garbage
break;
}
string key = connectionString.Substring(keyStart, i - keyStart).Trim();
i++; // skip '='
// Read value: quoted or unquoted
string value;
if (i < n && connectionString[i] == '"')
{
i++; // skip opening quote
int valStart = i;
bool foundEndQuote = false;
var sb = new System.Text.StringBuilder();
while (i < n)
{
char c = connectionString[i];
if (c == '"')
{
// Check for escaped double-quote ("") → "
if (i + 1 < n && connectionString[i + 1] == '"')
{
sb.Append('"');
i += 2;
continue;
}
foundEndQuote = true;
i++; // consume ending quote
break;
}
sb.Append(c);
i++;
}
value = sb.ToString();
// After quoted value, consume until next ';' or end
while (i < n && connectionString[i] != ';') i++;
if (i < n && connectionString[i] == ';') i++; // skip ';'
}
else
{
// Unquoted: read until ';' or end
int valStart = i;
while (i < n && connectionString[i] != ';') i++;
value = connectionString.Substring(valStart, i - valStart).Trim();
if (i < n && connectionString[i] == ';') i++; // skip ';'
}
if (!string.IsNullOrEmpty(key))
result[key] = value;
}
return result;
}
private static object? ConvertTo(string? raw, Type destinationType)
{
if (destinationType == typeof(string)) return raw;
if (string.IsNullOrEmpty(raw))
{
if (IsNullable(destinationType)) return null;
// Empty string into non-nullable → try default(T)
return destinationType.IsValueType ? Activator.CreateInstance(UnderlyingType(destinationType)) : null;
}
var nonNullableType = UnderlyingType(destinationType);
// Enums: support numeric and named (case-insensitive)
if (nonNullableType.IsEnum)
{
if (long.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var _))
return Enum.ToObject(nonNullableType, Convert.ToInt64(raw, CultureInfo.InvariantCulture));
return Enum.Parse(nonNullableType, raw, ignoreCase: true);
}
// Booleans: accept 1/0/true/false/yes/no
if (nonNullableType == typeof(bool))
{
if (bool.TryParse(raw, out var b)) return b;
if (raw.Equals("1", StringComparison.OrdinalIgnoreCase) ||
raw.Equals("yes", StringComparison.OrdinalIgnoreCase) ||
raw.Equals("y", StringComparison.OrdinalIgnoreCase) ||
raw.Equals("on", StringComparison.OrdinalIgnoreCase))
return true;
if (raw.Equals("0", StringComparison.OrdinalIgnoreCase) ||
raw.Equals("no", StringComparison.OrdinalIgnoreCase) ||
raw.Equals("n", StringComparison.OrdinalIgnoreCase) ||
raw.Equals("off", StringComparison.OrdinalIgnoreCase))
return false;
throw new FormatException($"Invalid boolean value '{raw}'.");
}
var converter = TypeDescriptor.GetConverter(nonNullableType);
if (converter != null && converter.CanConvertFrom(typeof(string)))
{
var val = converter.ConvertFrom(null, CultureInfo.InvariantCulture, raw);
return val;
}
// Fallback: try ChangeType
return Convert.ChangeType(raw, nonNullableType, CultureInfo.InvariantCulture);
}
private static bool IsNullable(Type t) => Nullable.GetUnderlyingType(t) != null || !t.IsValueType;
private static Type UnderlyingType(Type t) => Nullable.GetUnderlyingType(t) ?? t;
}