You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Connected.Framework/Connected.Data/FieldMappings.cs

193 lines
6.6 KiB

2 years ago
using Connected.Entities.Annotations;
using Connected.Interop;
using System.Data;
using System.Reflection;
namespace Connected.Data;
/// <summary>
/// Performs mapping between <see cref="IDataReader"/> fields and entity properties.
/// </summary>
/// <typeparam name="TEntity">The entity type to be used.</typeparam>
internal class FieldMappings<TEntity>
{
private Dictionary<int, PropertyInfo> _properties;
/// <summary>
/// Creates a new <see cref="FieldMappings{TEntity}"/> object.
/// </summary>
/// <param name="reader">The <see cref="IDataReader"/> providing entity records.</param>
public FieldMappings(IDataReader reader)
{
Initialize(reader);
}
/// <summary>
/// Cached properties use when looping through the records.
/// </summary>
private Dictionary<int, PropertyInfo> Properties => _properties;
/// <summary>
/// Initializes the mappings base on the provided <see cref="IDataReader"/>
/// and <typeparamref name="TEntity"/>
/// </summary>
/// <param name="reader">The active reader containing records.</param>
private void Initialize(IDataReader reader)
{
/*
* For primitive types there are no mappings since it's an scalar call.
*/
if (typeof(TEntity).IsTypePrimitive())
return;
_properties = new Dictionary<int, PropertyInfo>();
/*
* We are binding only properties, not fields.
*/
var properties = typeof(TEntity).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
for (var i = 0; i < reader.FieldCount; i++)
{
if (FieldMappings<TEntity>.ResolveProperty(properties, reader.GetName(i)) is PropertyInfo property)
_properties.Add(i, property);
}
}
/// <summary>
/// Creates a new instance of the <see cref="TEntity"/> and binds
/// properties from the provided <see cref="IDataReader"/>.
/// </summary>
/// <param name="reader">The <see cref="IDataReader"/> containing the record.</param>
/// <returns>A new instance of the <see cref="TEntity"/> with bound values from the <see cref="IDataReader"/>.</returns>
public TEntity? CreateInstance(IDataReader reader)
{
/*
* For primitive return values we'll use only the first field and return itž
* to the caller.
*/
if (typeof(TEntity).IsTypePrimitive())
{
if (reader.FieldCount == 0)
return default;
if (TypeConversion.TryConvert(reader[0], out TEntity? result))
return result;
return default;
}
/*
* It's an actual entity. First, create a new instance. Entities should have
* public parameterless constructor.
*/
if (typeof(TEntity?).CreateInstance<TEntity>() is not TEntity instance)
throw new NullReferenceException(typeof(TEntity).FullName);
foreach (var property in Properties)
Bind(instance, property, reader);
return instance;
}
/// <summary>
/// Resolves a correct property from the entity's properties based on a <see cref="IDataReader"/> field name.
/// </summary>
/// <param name="properties">The entity's properties.</param>
/// <param name="name">The <see cref="IDataReader"/> field name.</param>
/// <returns>A <see cref="PropertyInfo"/> if found, <c>null</c> otherwise.</returns>
private static PropertyInfo? ResolveProperty(PropertyInfo[] properties, string name)
{
/*
* There are two ways to map a property (evaluated in the following order):
* 1. from property name
* 2. from MemberAttribute
*
* We'll first perform case insensitive comparison because fields in the database are usually stored in a camelCase format.
*/
if (properties.FirstOrDefault(f => string.Equals(f.Name, name, StringComparison.OrdinalIgnoreCase)) is PropertyInfo property && property.CanWrite)
{
/*
* Property is found, examine if the persistence from the storage is supported.
*/
var att = property.FindAttribute<PersistenceAttribute>();
if (att is null || att.Persistence.HasFlag(ColumnPersistence.Read))
return property;
}
/*
* Property wasn't found, let's try to find it via MemberAttribute.
*/
foreach (var prop in properties)
{
/*
* It's case insensitive comparison again because we don't want bother user with exact matching. Since a database is probably case insensitive anyway
* there is no option to store columns with case sensitive names.
*/
if (prop.FindAttribute<MemberAttribute>() is MemberAttribute nameAttribute && string.Compare(nameAttribute.Member, name, true) == 0 && prop.CanWrite)
return prop;
}
/*
* Property could't be found. The field will be ognored when reading data.
*/
return default;
}
/// <summary>
/// Binds the <see cref="IDataReader"/> value to the entity's property.
/// </summary>
/// <param name="instance">The instance of the entity.</param>
/// <param name="property">The property on which value to be set.</param>
/// <param name="reader">The <see cref="IDataReader"/>providing the value.</param>
private static void Bind(object instance, KeyValuePair<int, PropertyInfo> property, IDataReader reader)
{
var value = reader.GetValue(property.Key);
/*
* We won't bind null values. We'll leave the property as is.
*/
if (value is null || Convert.IsDBNull(value))
return;
/*
* We have a few exceptions when binding values.
*/
if (property.Value.PropertyType == typeof(string) && value is byte[] bv)
{
/*
* If the property is string and the reader's value is byte array we are probably dealing
* with Consistency field. We'll first check if the property contains the attribute. If so,
* we'll convert byte array to eTag kind of value. If not we'll simply convert value to base64
* string.
*/
if (property.Value.FindAttribute<ETagAttribute>() is not null)
{
var versionValue = (EntityVersion?)bv;
if (versionValue is null)
value = Convert.ToBase64String(bv);
else
value = versionValue.ToString();
}
else
value = Convert.ToBase64String(bv);
}
else if (property.Value.PropertyType == typeof(DateTimeOffset))
{
/*
* We don't perform any conversions on dates. All dates should be stored in a UTC
* format so we simply set the correct kind of date so it can be later correctly
* converted
*/
value = new DateTimeOffset(DateTime.SpecifyKind((DateTime)value, DateTimeKind.Utc));
}
else if (property.Value.PropertyType == typeof(DateTime))
{
/*
* Like DateTimeOffset, the same is true for DateTime values
*/
value = DateTime.SpecifyKind((DateTime)value, DateTimeKind.Utc);
}
else
{
/*
* For other values we just perform a conversion.
*/
value = TypeConversion.Convert(value, property.Value.PropertyType);
}
/*
* Now bind the property from the converted value.
*/
property.Value.SetValue(instance, value);
}
}