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.
193 lines
6.6 KiB
193 lines
6.6 KiB
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);
|
|
}
|
|
}
|