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); } }