using Connected.Entities.Annotations;
using Connected.Interop;
using System.Data;
using System.Reflection;
namespace Connected.Data;
///
/// Performs mapping between fields and entity properties.
///
/// The entity type to be used.
internal class FieldMappings
{
private Dictionary _properties;
///
/// Creates a new object.
///
/// The providing entity records.
public FieldMappings(IDataReader reader)
{
Initialize(reader);
}
///
/// Cached properties use when looping through the records.
///
private Dictionary Properties => _properties;
///
/// Initializes the mappings base on the provided
/// and
///
/// The active reader containing records.
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();
/*
* 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.ResolveProperty(properties, reader.GetName(i)) is PropertyInfo property)
_properties.Add(i, property);
}
}
///
/// Creates a new instance of the and binds
/// properties from the provided .
///
/// The containing the record.
/// A new instance of the with bound values from the .
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() is not TEntity instance)
throw new NullReferenceException(typeof(TEntity).FullName);
foreach (var property in Properties)
Bind(instance, property, reader);
return instance;
}
///
/// Resolves a correct property from the entity's properties based on a field name.
///
/// The entity's properties.
/// The field name.
/// A if found, null otherwise.
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();
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() 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;
}
///
/// Binds the value to the entity's property.
///
/// The instance of the entity.
/// The property on which value to be set.
/// The providing the value.
private static void Bind(object instance, KeyValuePair 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() 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);
}
}