Files
adas-core/adas-core.Domain/Utils/Mapper.cs
T
2026-06-27 15:23:26 -07:00

77 lines
3.9 KiB
C#

using System.Dynamic;
using System.Reflection;
namespace adas_core.Domain.Utils;
/// <summary>
/// Provides a static mapping utility for instances of the reference type <typeparamref name="T"/>.
/// </summary>
/// <typeparam name="T">The reference type that this mapper operates on, constrained to reference types via the <c>class</c> constraint.</typeparam>
/// <remarks>
/// As indicated by the <c>where T : class</c> constraint, only reference types can be supplied as the generic argument for <typeparamref name="T"/>.
/// </remarks>
/// <!-- aidoc:v1 sig=4a87291 -->
public static class Mapper<T>
// We can only use reference types
where T : class
{
private static readonly Dictionary<string, PropertyInfo> PropertyMap;
/// <summary>
/// Initializes the static <see cref="Mapper{T}.PropertyMap"/> cache used by the mapper to look up <see cref="System.Reflection.PropertyInfo"/> entries for the type parameter T by their lowercased property name.
/// </summary>
/// <!-- aidoc:v1 sig=ed9693b body=3dc0e55 -->
static Mapper()
{
// At this point we can convert each
// property name to lower case so we avoid
// creating a new string more than once.
PropertyMap =
typeof(T)
.GetProperties()
.ToDictionary(
p => p.Name.ToLower(),
p => p
);
}
/// <summary>
/// Maps properties from an <see cref="ExpandoObject"/> source to a strongly-typed destination object using a pre-defined property map.
/// Property lookups are case-insensitive. Null values are only assigned to reference types or <see cref="Nullable{T}"/> types; assigning null to a non-nullable value type throws. When a source value's type does not match the destination property's type, the value is converted using <see cref="Convert.ToDouble(object)"/>.
/// </summary>
/// <param name="source">The source <see cref="ExpandoObject"/> whose key-value pairs will be mapped onto the destination.</param>
/// <param name="destination">The target object that will receive the mapped property values.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="source"/> or <paramref name="destination"/> is null.</exception>
/// <exception cref="ArgumentException">Thrown when a source value is null but the corresponding destination property is a value type that is not <see cref="Nullable{T}"/>.</exception>
public static void Map(ExpandoObject source, T destination)
{
// Might as well take care of null references early.
if (source == null) throw new ArgumentNullException(nameof(source));
if (destination == null) throw new ArgumentNullException(nameof(destination));
// By iterating the KeyValuePair<string, object> of
// source we can avoid manually searching the keys of
// source as we see in your original code.
foreach (var kv in source)
if (PropertyMap.TryGetValue(kv.Key.ToLower(), out var p))
{
var propType = p.PropertyType;
if (kv.Value == null)
{
if (propType is { IsByRef: false } && propType.Name != "Nullable`1")
// Throw if type is a value type
// but not Nullable<>
throw new ArgumentException("not nullable");
}
else if (kv.Value.GetType() != propType)
{
// You could make this a bit less strict
// but I don't recommend it.
p.SetValue(destination, Convert.ToDouble(kv.Value), null);
}
p.SetValue(destination, kv.Value, null);
}
}
}